package core import ( "encoding/json" "errors" "net/http" "github.com/go-redis/redis/v8" "github.com/gorilla/websocket" "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 bson.M) (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) removeMemberByTid(tid string) error { counts, _ := gd.rh.JSONNumIncrBy(gd.strid(), "$._count", -1) if len(counts) == 0 { // 이미 지워진 인스턴트그룹 return errGroupAlreadyDestroyed } if _, err := gd.rh.JSONDel(gd.strid(), "$._members."+tid); err != nil { return err } gd.Count = counts[0] return nil } func (gd *instantDoc) removeMember(mid accountID) error { return gd.removeMemberByTid(gd.tid(mid)) } func (gd *instantDoc) getMembers() (map[string]any, error) { res, err := gd.rh.JSONGet(gd.strid(), "$._members") 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[string]any) for k, v := range temp[0] { out[gd.mid(k).Hex()] = 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(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 } character := data.Character gid := data.Gid mid := data.Mid if gid.IsZero() || mid.IsZero() { logger.Println("JoinParty failed. mid should be exist") w.WriteHeader(http.StatusBadRequest) return } gd, err := gi.find(gid) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if gd == nil { // 그룹이 없다. 실패 w.WriteHeader(http.StatusBadRequest) return } // 내 정보 업데이트할 때에도 사용됨 if memdoc, err := gd.addMember(mid, character); err == nil { // 기존 유저에게 새 유저 알림 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gid.Hex(), Body: map[string]any{ gd.tid(mid): memdoc, }, Tag: []string{"MemberDocFull"}, }) gi.enterRoom(gid, mid) gi.rh.JSONSet(mid.Hex(), "$.instant", bson.M{"id": gd.strid()}) // 최초 입장이라면 새 멤버에 그룹 전체를 알림 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: mid.Hex(), Body: gd.loadFull(), Tag: []string{"GroupDocFull"}, }) } else if err != nil { logger.Error("JoinParty failed :", err) w.WriteHeader(http.StatusInternalServerError) } 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(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 := gd.removeMember(data.Mid); err != nil { if err == errGroupAlreadyDestroyed { // 정상 gocommon.MakeEncoder(w, r).Encode(int64(0)) return } logger.Println("groupInstant.Leave failed. gd.removeMember returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } gi.rh.JSONDel(data.Mid.Hex(), "$.instant.id") // mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: data.Mid.Hex(), Body: bson.M{"_gid": data.Gid}, Tag: []string{"GroupDocFull", gd.strid()}, }) // gid에는 제거 메시지 보냄 gi.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gd.strid(), Body: bson.M{ gd.tid(data.Mid): bson.M{}, }, Tag: []string{"MemberDocFull"}, }) gi.leaveRoom(gd.Gid, data.Mid) if gd.Count == 0 { gd.rh.Del(gd.rh.Context(), gd.strid()).Result() } gocommon.MakeEncoder(w, r).Encode(gd.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(conn *websocket.Conn, 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() } } }