package core import ( "encoding/json" "errors" "net/http" "github.com/go-redis/redis/v8" "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/logger" "repositories.action2quare.com/ayo/gocommon/wshandler" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) type instantDoc struct { Members map[string]any `json:"_members"` Count int64 `json:"_count"` Body primitive.M `json:"_body"` Gid primitive.ObjectID `json:"_gid"` rh *gocommon.RedisonHandler idstr string } func (gd *instantDoc) loadMemberFull(tid string) (bson.M, error) { full, err := gd.rh.JSONGet(gd.strid(), "$._members."+tid) if err != nil { return nil, err } bt := []byte(full.(string)) bt = bt[1 : len(bt)-1] var doc bson.M if err = json.Unmarshal(bt, &doc); err != nil { return nil, err } return doc, nil } func (gd *instantDoc) loadFull() (doc bson.M) { // 새 멤버에 그룹 전체를 알림 full, err := gd.rh.JSONGet(gd.strid(), "$") if err == nil { bt := []byte(full.(string)) bt = bt[1 : len(bt)-1] err = json.Unmarshal(bt, &doc) if err != nil { logger.Println("loadFull err :", err) } } else { logger.Println("loadFull err :", err) } return } func (gd *instantDoc) strid() string { if len(gd.idstr) == 0 { gd.idstr = gd.Gid.Hex() } return gd.idstr } func (gd *instantDoc) tid(in accountID) string { return makeTid(gd.Gid, in) } func (gd *instantDoc) mid(tid string) accountID { tidobj, _ := primitive.ObjectIDFromHex(tid) var out primitive.ObjectID for i := range tidobj { out[12-i-1] = gd.Gid[i] ^ tidobj[12-i-1] } return out } func (gd *instantDoc) addMember(mid accountID, character any) (bson.M, error) { tid := gd.tid(mid) if _, err := gd.rh.JSONSet(gd.strid(), "$._members."+tid, character); err != nil { return nil, err } counts, err := gd.rh.JSONNumIncrBy(gd.strid(), "$._count", 1) if err != nil { return nil, err } gd.Count = counts[0] return gd.loadMemberFull(tid) } var errGroupAlreadyDestroyed = errors.New("instant group is already destroyed") func (gd *instantDoc) removeMember(mid accountID) error { counts, _ := gd.rh.JSONNumIncrBy(gd.strid(), "$._count", -1) if len(counts) == 0 { // 이미 지워진 인스턴트그룹 return errGroupAlreadyDestroyed } if _, err := gd.rh.JSONDel(gd.strid(), "$._members."+gd.tid(mid)); err != nil { return err } gd.Count = counts[0] return nil } func (gd *instantDoc) getMembers() (map[primitive.ObjectID]any, error) { res, err := gd.rh.JSONGet(gd.strid(), "$._members") if err == redis.Nil { return nil, nil } if err != nil { return nil, err } var temp []map[string]any err = json.Unmarshal([]byte(res.(string)), &temp) if err != nil { return nil, err } out := make(map[primitive.ObjectID]any) for k, v := range temp[0] { out[gd.mid(k)] = v } return out, nil } type groupInstant struct { sendUpstreamMessage func(*wshandler.UpstreamMessage) enterRoom func(groupID, accountID) leaveRoom func(groupID, accountID) rh *gocommon.RedisonHandler } func (gi *groupInstant) Initialize(tv *Tavern) error { gi.rh = tv.redison gi.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) { tv.wsh.SendUpstreamMessage(msg) } gi.enterRoom = func(gid groupID, accid accountID) { tv.wsh.EnterRoom(gid.Hex(), accid) } gi.leaveRoom = func(gid groupID, accid accountID) { tv.wsh.LeaveRoom(gid.Hex(), accid) } return nil } func (gi *groupInstant) RegisterApiFunctions() { } func (gi *groupInstant) join(gd *instantDoc, mid primitive.ObjectID, character any) error { // 내 정보 업데이트할 때에도 사용됨 memdoc, err := gd.addMember(mid, character) if err != nil { return err } delete(memdoc, "_id") // 기존 유저에게 새 유저 알림 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gd.strid(), Body: map[string]any{ gd.tid(mid): memdoc, }, Tag: []string{"MemberDocFull"}, }) gi.rh.JSONSet(mid.Hex(), "$.instant", bson.M{"id": gd.strid()}) full := gd.loadFull() if f, ok := full["_members"]; ok { members := f.(map[string]any) for _, char := range members { delete(char.(map[string]any), "_id") } } // 최초 입장이라면 새 멤버에 그룹 전체를 알림 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: mid.Hex(), Body: full, Tag: []string{"GroupDocFull"}, }) gi.enterRoom(gd.Gid, mid) return nil } func (gi *groupInstant) UpdateInstantDocument(w http.ResponseWriter, r *http.Request) { var data struct { Gid primitive.ObjectID Doc primitive.M Result string } if err := gocommon.MakeDecoder(r).Decode(&data); err != nil { logger.Println("UpdateInstantDocument failed. Decode returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } gd := partyDoc{ id: data.Gid, rh: gi.rh, } if err := gi.rh.JSONMSetRel(gd.strid(), "$.", data.Doc); err != nil { logger.Println("UpdateInstantDocument failed. JSONMSetRel returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } // 업데이트 알림 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gd.strid(), Body: data.Doc, Tag: []string{"GroupDocFragment"}, }) if data.Result == "after" { fulldoc := gd.loadFull() if fulldoc != nil { tids := fulldoc["_members"].(map[string]any) mids := make(map[string]any) for k, v := range tids { mid := midFromTid(data.Gid, k) mids[mid.Hex()] = v } fulldoc["_members"] = mids } gocommon.MakeEncoder(w, r).Encode(fulldoc) } } func (gi *groupInstant) Join(w http.ResponseWriter, r *http.Request) { var data struct { Gid primitive.ObjectID Mid primitive.ObjectID Character primitive.M } if err := gocommon.MakeDecoder(r).Decode(&data); err != nil { logger.Println("JoinParty failed. DecodeGob returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } if data.Gid.IsZero() || data.Mid.IsZero() { logger.Println("groupInstant.Join failed. gid or mid is zero") w.WriteHeader(http.StatusBadRequest) return } gd, err := gi.find(data.Gid) if err != nil || gd == nil { logger.Println("groupInstant.Join failed. gi find return err :", err) w.WriteHeader(http.StatusInternalServerError) return } if err := gi.join(gd, data.Mid, data.Character); err != nil { logger.Println("groupInstant.Join failed :", err) w.WriteHeader(http.StatusInternalServerError) } else { gocommon.MakeEncoder(w, r).Encode(gd.Count) } } func (gi *groupInstant) Create(w http.ResponseWriter, r *http.Request) { var data struct { Mid primitive.ObjectID Body primitive.M Character primitive.M } if err := gocommon.MakeDecoder(r).Decode(&data); err != nil { logger.Println("CreateParty failed. Decode returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } gd, err := gi.createInstantGroup(data.Mid, data.Character, data.Body) if err != nil { logger.Println("groupInstant.Create failed. gp.createInstantGroup() return err :", err) w.WriteHeader(http.StatusInternalServerError) return } // 내가 wshandler room에 입장 gi.enterRoom(gd.Gid, data.Mid) gi.rh.JSONSet(data.Mid.Hex(), "$.instant", bson.M{"id": gd.strid()}) gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: data.Mid.Hex(), Body: gd, Tag: []string{"GroupDocFull"}, }) gocommon.MakeEncoder(w, r).Encode(gd.Gid) } func (gi *groupInstant) Delete(w http.ResponseWriter, r *http.Request) { var gid primitive.ObjectID if err := gocommon.MakeDecoder(r).Decode(&gid); err != nil { logger.Println("CreateParty failed. Decode returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } } func (gi *groupInstant) leave(gd *instantDoc, mid primitive.ObjectID) error { if err := gd.removeMember(mid); err != nil { if err == errGroupAlreadyDestroyed { // 정상 gd.Count = 0 return nil } return err } gi.rh.JSONDel(mid.Hex(), "$.instant.id") // gid에는 제거 메시지 보냄 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gd.strid(), Body: bson.M{ gd.tid(mid): bson.M{}, }, Tag: []string{"MemberDocFull"}, }) gi.leaveRoom(gd.Gid, mid) if gd.Count == 0 { gd.rh.Del(gd.rh.Context(), gd.strid()).Result() } return nil } func (gi *groupInstant) Leave(w http.ResponseWriter, r *http.Request) { var data struct { Gid primitive.ObjectID Mid primitive.ObjectID } if err := gocommon.MakeDecoder(r).Decode(&data); err != nil { logger.Println("RemoveFromParty failed. Decode returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } gd := instantDoc{ Gid: data.Gid, rh: gi.rh, } if err := gi.leave(&gd, data.Mid); err != nil { logger.Println("groupInstant.Leave failed. gd.removeMember returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } gocommon.MakeEncoder(w, r).Encode(gd.Count) } func (gi *groupInstant) Merge(w http.ResponseWriter, r *http.Request) { var data struct { From primitive.ObjectID Into primitive.ObjectID Max int64 } if err := gocommon.MakeDecoder(r).Decode(&data); err != nil { logger.Println("RemoveFromParty failed. Decode returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } // From에 있는 mid를 Into로 옮김 gdinto, err := gi.find(data.Into) if err != nil { logger.Println("groupInstant.Merge failed. gd.getMembers returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } if gdinto == nil { // 이미 나갔다. 머지 중단 gocommon.MakeEncoder(w, r).Encode(struct { From int64 Into int64 }{From: -1, Into: 0}) // -1: 알수 없음, 0: 비었음 return } gdfrom := instantDoc{ Gid: data.From, rh: gi.rh, } fromMembers, err := gdfrom.getMembers() if err != nil { logger.Println("groupInstant.Merge failed. gd.getMembers returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } if len(fromMembers) == 0 { // gdfrom이 비었다. 머지 중단 gocommon.MakeEncoder(w, r).Encode(struct { From int64 Into int64 }{From: 0, Into: -1}) return } var movedmids []primitive.ObjectID for mid, doc := range fromMembers { gi.join(gdinto, mid, doc) gi.leaveRoom(gdfrom.Gid, mid) movedmids = append(movedmids, mid) if gdinto.Count == data.Max { break } } if len(movedmids) == int(gdfrom.Count) { gi.rh.JSONDel(gdfrom.strid(), "$") } else { for _, mid := range movedmids { gdfrom.removeMember(mid) // gid에는 제거 메시지 보냄 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gdfrom.strid(), Body: bson.M{ gdfrom.tid(mid): bson.M{}, }, Tag: []string{"MemberDocFull"}, }) } } gocommon.MakeEncoder(w, r).Encode(struct { From int64 Into int64 }{From: gdfrom.Count, Into: gdinto.Count}) } func (gi *groupInstant) createInstantGroup(firstAcc primitive.ObjectID, firstChar primitive.M, instDoc primitive.M) (*instantDoc, error) { newid := primitive.NewObjectID() tid := makeTid(newid, firstAcc) gd := &instantDoc{ Members: map[string]any{ tid: firstChar, }, Body: instDoc, Count: 1, rh: gi.rh, Gid: newid, } _, err := gi.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX) if err != nil { return nil, err } return gd, nil } func (gi *groupInstant) find(id groupID) (*instantDoc, error) { if id.IsZero() { return nil, nil } _, err := gi.rh.JSONObjLen(id.Hex(), "$") if err == redis.Nil { return nil, nil } if err != nil { return nil, err } return &instantDoc{ rh: gi.rh, Gid: id, }, nil } func (gi *groupInstant) ClientDisconnected(msg string, callby *wshandler.Sender) { gids, _ := gi.rh.JSONGetString(callby.Accid.Hex(), "$.instant.id") if len(gids) > 0 && len(gids[0]) > 0 { gidstr := gids[0] gid, _ := primitive.ObjectIDFromHex(gidstr) gd := instantDoc{ Gid: gid, rh: gi.rh, } gi.rh.JSONDel(callby.Accid.Hex(), "$.instant.id") if err := gd.removeMember(callby.Accid); err != nil { if err == errGroupAlreadyDestroyed { // 정상 return } logger.Println("ClientDisconnected failed. gd.removeMember returns err :", err) return } // gid에는 제거 메시지 보냄 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gd.strid(), Body: bson.M{ gd.tid(callby.Accid): bson.M{}, }, Tag: []string{"MemberDocFull"}, }) gi.leaveRoom(gd.Gid, callby.Accid) if gd.Count == 0 { gd.rh.Del(gd.rh.Context(), gd.strid()).Result() } } }