package core import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" "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 accountID = primitive.ObjectID type groupID = primitive.ObjectID func makeTid(gid groupID, in accountID) string { var out primitive.ObjectID for i := range in { out[12-i-1] = gid[i] ^ in[12-i-1] } return out.Hex() } func midFromTid(gid groupID, in string) accountID { h, _ := primitive.ObjectIDFromHex(in) var out accountID for i := range h { out[12-i-1] = gid[i] ^ h[12-i-1] } return out } type Invitation struct { GroupID groupID `json:"_gid"` TicketID string `json:"_tid"` Inviter bson.M `json:"_inviter"` // memberDoc.Body ExpireAtUTC int64 `json:"_expire_at_utc"` } // 플레이어한테 공유하는 멤버 정보 type memberDoc struct { Body bson.M `json:"_body"` Invite bool `json:"_invite"` InviteExpire int64 `json:"_invite_exp"` } type InvitationFail bson.M type partyDoc struct { Members map[string]any `json:"_members"` InCharge string `json:"_incharge"` Gid string `json:"_gid"` rh *gocommon.RedisonHandler id groupID } func (gd *partyDoc) 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 *partyDoc) 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 *partyDoc) strid() string { if len(gd.Gid) == 0 { gd.Gid = gd.id.Hex() } return gd.Gid } func (gd *partyDoc) tid(in accountID) string { return makeTid(gd.id, in) } func (gd *partyDoc) mid(tid string) accountID { tidobj, _ := primitive.ObjectIDFromHex(tid) var out primitive.ObjectID for i := range tidobj { out[12-i-1] = gd.id[i] ^ tidobj[12-i-1] } return out } func (gd *partyDoc) addInvite(mid accountID, body bson.M, ttl time.Duration, max int) (*memberDoc, error) { targetmid := mid targetbody := body // 초대 가능한 빈 자리가 있나 tids, err := gd.rh.JSONObjKeys(gd.strid(), "$._members") if err != nil { return nil, err } now := time.Now().UTC() createNewDoc := func() *memberDoc { return &memberDoc{ Body: targetbody, Invite: true, InviteExpire: now.Add(ttl).Unix(), } } newtid := gd.tid(targetmid) if len(tids) < max { // 빈자리를 찾았다. newdoc := createNewDoc() _, err := gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc) return newdoc, err } expires, err := gd.rh.JSONGetInt64(gd.strid(), "$._members.._invite_exp") if err != nil { return nil, err } var delpaths []string for i, expire := range expires { if expire < now.Unix() { // 만료된 초대가 있네? 지우자 delpaths = append(delpaths, "$._members."+tids[i]) } } if len(delpaths) == 0 { // 빈자리가 없다 return nil, nil } if err := gd.rh.JSONMDel(gd.strid(), delpaths); err != nil { return nil, err } newdoc := createNewDoc() _, err = gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc) return newdoc, err } func (gd *partyDoc) addMember(mid accountID, character bson.M) (bson.M, error) { tid := gd.tid(mid) prefix := "$._members." + tid if _, err := gd.rh.JSONSet(gd.strid(), prefix+"._body", character, gocommon.RedisonSetOptionXX); err != nil { return nil, err } if err := gd.rh.JSONMDel(gd.strid(), []string{prefix + "._invite", prefix + "._invite_exp"}); err != nil { return nil, err } gd.rh.Persist(gd.rh.Context(), gd.strid()).Result() return gd.loadMemberFull(tid) } func (gd *partyDoc) getIncharge() string { if len(gd.InCharge) == 0 { incharge, err := gd.rh.JSONGet(gd.strid(), "$._incharge") if err == nil && incharge != nil { gd.InCharge = strings.Trim(incharge.(string), "[]\"") } } return gd.InCharge } func (gd *partyDoc) removeMemberByTid(tid string) error { _, err := gd.rh.JSONDel(gd.strid(), "$._members."+tid) if err != nil { return err } counts, err := gd.rh.JSONObjLen(gd.strid(), "$._members") if err != nil { return err } if len(counts) > 0 && counts[0] == 0 { _, err = gd.rh.Del(gd.rh.Context(), gd.strid()).Result() } return err } func (gd *partyDoc) removeMember(mid accountID) error { return gd.removeMemberByTid(gd.tid(mid)) } func (gd *partyDoc) 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] { body := v.(map[string]any)["_body"] out[gd.mid(k).Hex()] = body } return out, nil } type partyConfig struct { InviteExpire int32 `json:"invite_ttl"` // 그룹이 개인에게 보낸 초대장 만료 기한 MaxMember int `json:"max_member"` Name string } type groupParty struct { partyConfig sendUpstreamMessage func(*wshandler.UpstreamMessage) enterRoom func(groupID, accountID) leaveRoom func(groupID, accountID) rh *gocommon.RedisonHandler } func (gp *groupParty) Initialize(tv *Tavern, cfg configDocument) error { rem, _ := json.Marshal(cfg) err := json.Unmarshal(rem, &gp.partyConfig) if err != nil { return err } gp.rh = tv.redison gp.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) { tv.wsh.SendUpstreamMessage(msg) } gp.enterRoom = func(gid groupID, accid accountID) { tv.wsh.EnterRoom(gid.Hex(), accid) } gp.leaveRoom = func(gid groupID, accid accountID) { tv.wsh.LeaveRoom(gid.Hex(), accid) } return nil } // JoinParty : 그룹에 참가 // - type : 그룹 타입 // - 그룹 타입에 맞는 키(주로 _id) // - member_id : 참가 멤버의 아이디 // - body : 멤버의 속성 bson document func (gp *groupParty) JoinParty(w http.ResponseWriter, r *http.Request) { var data struct { Gid primitive.ObjectID Mid primitive.ObjectID First bool Character bson.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 := gp.find(gid) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if gd == nil { // 그룹이 없다. 없을 수도 있지 gocommon.MakeEncoder(w, r).Encode("") return } // 내 정보 업데이트할 때에도 사용됨 if !data.First { // 이미 멤버여야 재입장 가능 path := "$._members." + gd.tid(mid) + "._body" if _, err := gd.rh.JSONSet(gd.strid(), path, character, gocommon.RedisonSetOptionXX); err != nil { // 멤버가 아니네? 그새 파티장이 쫓아냈을 수도 있다. gocommon.MakeEncoder(w, r).Encode("") return } } gp.rh.JSONSet(mid.Hex(), "$.party", bson.M{"id": gid.Hex()}) memdoc, err := gd.addMember(mid, character) if err != nil { logger.Println("JoinParty failed :", err) w.WriteHeader(http.StatusInternalServerError) return } // 기존 유저에게 새 유저 알림 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gid.Hex(), Body: map[string]any{ gd.tid(mid): memdoc, }, Tag: []string{"MemberDocFull"}, }) gp.enterRoom(gid, mid) gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: mid.Hex(), Body: gd.loadFull(), Tag: []string{"GroupDocFull"}, }) gocommon.MakeEncoder(w, r).Encode(gd.strid()) } func (gp *groupParty) ConditionalClearPartyMember(w http.ResponseWriter, r *http.Request) { var doc struct { Gid string Mid string } // accid가 접속해 있지 않으면 파티에서 나간 걸로 간주하고 // accid가 접속해 있으면 아무것도 하지 않는다. if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil { logger.Println("ConditionalClearPartyMember failed. DecodeGob returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } pids, err := gp.rh.JSONGetString(doc.Mid, "$.party.id") if err != nil { logger.Println("ConditionalClearPartyMember failed. gp.rh.JSONGetString returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } removeMember := func() { gid, _ := primitive.ObjectIDFromHex(doc.Gid) mid, _ := primitive.ObjectIDFromHex(doc.Mid) gd := partyDoc{ id: gid, rh: gp.rh, } if gd.getIncharge() == gd.tid(mid) { // 방장이 나갔다. gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + doc.Gid, Body: bson.M{"gid": gid}, Tag: []string{"GroupDocFull", gid.Hex()}, }) gd.rh.Del(gd.rh.Context(), gd.strid()).Result() } else { gd.removeMember(mid) gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + doc.Gid, Body: bson.M{ gd.tid(mid): bson.M{}, }, Tag: []string{"MemberDocFull"}, }) } } if len(pids) == 0 { // 없다. // doc.Gid에서 제거 removeMember() } else if pids[0] != doc.Gid { // 다른 파티? 기존 파티에서 제거 removeMember() } } // InviteToParty : 초대 // - type : 초대 타입 (required) // - from : 초대하는 자 (required) // - to : 초대받는 자 (required) // - timeout : 초대 유지시간(optional. 없으면 config 기본 값) // - (body) : 검색시 노출되는 document func (gp *groupParty) InviteToParty(w http.ResponseWriter, r *http.Request) { var doc struct { Gid primitive.ObjectID Mid primitive.ObjectID Target primitive.ObjectID Inviter bson.M Invitee bson.M InitialPartyDoc bson.M } if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil { logger.Println("InviteToParty failed. DecodeGob returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } targetid := doc.Target gid := doc.Gid mid := doc.Mid // targetid에 초대한 mid가 들어있다. success, err := gp.rh.SetNX(context.Background(), "inv."+targetid.Hex(), mid.Hex(), time.Duration(gp.InviteExpire)*time.Second).Result() if err != nil { logger.Println("InviteToParty failed. gp.rh.SetNX() return err :", err) w.WriteHeader(http.StatusInternalServerError) return } if !success { // 이미 초대 중이다. // inviter한테 알려줘야 한다. gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: mid.Hex(), Body: doc.Invitee, Tag: []string{"InvitationFail"}, }) return } gd, err := gp.find(gid) if err != nil { logger.Println("InviteToParty failed. gp.find() return err :", err) w.WriteHeader(http.StatusBadRequest) return } if gd == nil { gid = primitive.NewObjectID() gd, err = gp.createGroup(gid, mid, doc.Inviter, doc.InitialPartyDoc) if err != nil { logger.Println("InviteToParty failed. gp.createGroup() return err :", err) w.WriteHeader(http.StatusInternalServerError) return } // 내가 wshandler room에 입장 gp.enterRoom(gid, mid) gp.rh.JSONSet(mid.Hex(), "$.party", bson.M{"id": gid.Hex()}) gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: mid.Hex(), Body: gd, Tag: []string{"GroupDocFull"}, }) } newdoc, err := gd.addInvite(targetid, doc.Invitee, time.Duration(gp.InviteExpire+1)*time.Second, gp.MaxMember) if err != nil { logger.Println("InviteToParty failed. gp.addInvite() return err :", err) w.WriteHeader(http.StatusInternalServerError) return } // invitee에게 알림 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: targetid.Hex(), Body: Invitation{ GroupID: gid, TicketID: gd.tid(targetid), Inviter: doc.Inviter, ExpireAtUTC: newdoc.InviteExpire, }, Tag: []string{"Invitation"}, }) // 그룹에게 알림 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gid.Hex(), Body: bson.M{ gd.tid(targetid): bson.M{ "nickname": doc.Invitee["nickname"], "_invite": true, }, }, Tag: []string{"MemberDocFull"}, }) } func (gp *groupParty) UpdatePartyMemberDocument(w http.ResponseWriter, r *http.Request) { var doc struct { Gid string Tid string Fragment bson.M } if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil { logger.Println("UpdatePartyMemberDocument failed. DecodeGob returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } gidobj, _ := primitive.ObjectIDFromHex(doc.Gid) mid := midFromTid(gidobj, doc.Tid) gp.updateMemberDocument(gidobj, mid, doc.Fragment) } func (gp *groupParty) AcceptPartyInvitation(w http.ResponseWriter, r *http.Request) { var doc struct { Gid primitive.ObjectID Mid primitive.ObjectID Tid string Character bson.M } if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil { logger.Println("AcceptPartyInvitation failed. DecodeGob returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } gid := doc.Gid mid := doc.Mid member := doc.Character cnt, err := gp.rh.Del(context.Background(), "inv."+mid.Hex()).Result() if err != nil { logger.Println("AcceptPartyInvitation failed. gp.rh.Del returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } if cnt == 0 { // 만료됨 w.WriteHeader(http.StatusGatewayTimeout) return } pids, err := gp.rh.JSONGetString(mid.Hex(), "$.party.id") if err != nil { logger.Println("AcceptPartyInvitation failed. gp.rh.JSONGetString returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } if len(pids) > 0 && len(pids[0]) > 0 { // 기존에 이미 파티에 들어가 있다. // 기존 파티에서는 탈퇴 oldgid, _ := primitive.ObjectIDFromHex(pids[0]) oldgd := &partyDoc{ id: oldgid, rh: gp.rh, } // gid에는 제거 메시지 보냄 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + oldgd.strid(), Body: bson.M{ oldgd.tid(mid): bson.M{}, }, Tag: []string{"MemberDocFull"}, }) gp.leaveRoom(oldgid, mid) } gd := &partyDoc{ id: gid, rh: gp.rh, } memberDoc, err := gd.addMember(mid, member) if err == nil { // 기존 멤버에게 새 멤버를 알림 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gid.Hex(), Body: map[string]any{ gd.tid(mid): memberDoc, }, Tag: []string{"MemberDocFull"}, }) gp.enterRoom(gid, mid) // 현재 내 파티를 기록 gp.rh.JSONSet(mid.Hex(), "$.party", bson.M{"id": gid.Hex()}) // 새 멤버에 그룹 전체를 알림 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: mid.Hex(), Body: gd.loadFull(), Tag: []string{"GroupDocFull"}, }) } else { logger.Println("AcceptPartyInvitation failed. group.AcceptPartyInvitation returns err :", err) w.WriteHeader(http.StatusInternalServerError) } } func (gp *groupParty) QueryPartyMemberState(w http.ResponseWriter, r *http.Request) { var mid primitive.ObjectID if err := gocommon.MakeDecoder(r).Decode(&mid); err != nil { logger.Println("DenyPartyInvitation failed. DecodeGob returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } if cnt, _ := gp.rh.Exists(gp.rh.Context(), mid.Hex()).Result(); cnt == 0 { return } states, _ := gp.rh.JSONGetString(mid.Hex(), "$.party.state") if len(states) > 0 && len(states[0]) > 0 { gocommon.MakeEncoder(w, r).Encode(states[0]) } else { gocommon.MakeEncoder(w, r).Encode("connected") } } func (gp *groupParty) updateMemberDocument(gid groupID, mid accountID, doc bson.M) error { gd := &partyDoc{ id: gid, rh: gp.rh, } prefixPath := fmt.Sprintf("$._members.%s._body.", gd.tid(mid)) err := gp.rh.JSONMSetRel(gd.strid(), prefixPath, doc) if err != nil { return err } if newstate, ok := doc["_state"]; ok { gp.rh.JSONSet(mid.Hex(), "$.party.state", newstate) } gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gid.Hex(), Body: map[string]any{ gd.tid(mid): doc, }, Tag: []string{"MemberDocFragment"}, }) return nil } func (gp *groupParty) updatePartyDocument(gid groupID, frag bson.M) error { gd := partyDoc{ id: gid, rh: gp.rh, } if err := gp.rh.JSONMSetRel(gd.strid(), "$.", frag); err != nil { return err } // 업데이트 알림 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gid.Hex(), Body: frag, Tag: []string{"GroupDocFragment"}, }) return nil } func (gp *groupParty) UpdatePartyDocument(w http.ResponseWriter, r *http.Request) { var data struct { Gid primitive.ObjectID Doc bson.M } if err := gocommon.MakeDecoder(r).Decode(&data); err != nil { logger.Println("UpdatePartyDocument failed. DecodeGob returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } gid := data.Gid frag := data.Doc if err := gp.updatePartyDocument(gid, frag); err != nil { logger.Println("UpdatePartyDocument failed. group.UpdatePartyDocument returns err :", err) w.WriteHeader(http.StatusBadRequest) return } } func (gp *groupParty) QueryPartyMembers(w http.ResponseWriter, r *http.Request) { var gid primitive.ObjectID if err := gocommon.MakeDecoder(r).Decode(&gid); err != nil { logger.Println("QueryPartyMembers failed. DecodeGob returns err :", err) w.WriteHeader(http.StatusInternalServerError) return } gd := partyDoc{ id: gid, rh: gp.rh, } members, err := gd.getMembers() if err != nil { logger.Println("QueryPartyMembers failed. group.QueryPartyMembers returns err :", err) w.WriteHeader(http.StatusBadRequest) return } if err := gocommon.MakeEncoder(w, r).Encode(members); err != nil { logger.Println("QueryPartyMembers failed. writeBsonDoc return err :", err) w.WriteHeader(http.StatusInternalServerError) return } } func (gp *groupParty) createGroup(newid groupID, charge accountID, chargeDoc bson.M, initialGroupDoc bson.M) (*partyDoc, error) { tid := makeTid(newid, charge) gd := &partyDoc{ Members: map[string]any{ tid: &memberDoc{ Body: chargeDoc, Invite: false, InviteExpire: 0, }, }, InCharge: tid, Gid: newid.Hex(), rh: gp.rh, id: newid, } var err error if initialGroupDoc != nil { initialGroupDoc["_members"] = gd.Members initialGroupDoc["_incharge"] = gd.InCharge initialGroupDoc["_gid"] = gd.Gid _, err = gp.rh.JSONSet(gd.strid(), "$", initialGroupDoc, gocommon.RedisonSetOptionNX) } else { _, err = gp.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX) } if err != nil { return nil, err } return gd, nil } func (gp *groupParty) find(id groupID) (*partyDoc, error) { if id.IsZero() { return nil, nil } _, err := gp.rh.JSONObjLen(id.Hex(), "$") if err == redis.Nil { return nil, nil } if err != nil { return nil, err } return &partyDoc{ rh: gp.rh, id: id, }, nil } func (gp *groupParty) ClientDisconnected(msg string, callby *wshandler.Sender) { gids, _ := gp.rh.JSONGetString(callby.Accid.Hex(), "$.party.id") if len(gids) > 0 && len(gids[0]) > 0 { // mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 gidstr := gids[0] gid, _ := primitive.ObjectIDFromHex(gidstr) // 나를 먼저 룸에서 빼야 나한테 메시지가 안감 gp.leaveRoom(gid, callby.Accid) if msg != "pending" { gd := &partyDoc{ rh: gp.rh, id: gid, } if gd.getIncharge() == gd.tid(callby.Accid) { // 방장이 나감. 방 폭파 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gidstr, Body: bson.M{"gid": gid}, Tag: []string{"GroupDocFull", gidstr}, }) gd.rh.Del(gd.rh.Context(), gd.strid()).Result() } else { // gid에는 제거 메시지 보냄 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gidstr, Body: bson.M{ makeTid(gid, callby.Accid): bson.M{}, }, Tag: []string{"MemberDocFull"}, }) } } } } func (gp *groupParty) UpdatePartyMemberDocumentDirect(ctx wshandler.ApiCallContext) { gidobj, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) doc := ctx.Arguments[1].(map[string]any) gp.updateMemberDocument(gidobj, ctx.CallBy.Accid, doc) } func (gp *groupParty) UpdatePartyDocumentDirect(ctx wshandler.ApiCallContext) { gidobj, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) doc := ctx.Arguments[1].(map[string]any) gp.updatePartyDocument(gidobj, doc) } func (gp *groupParty) LeaveParty(ctx wshandler.ApiCallContext) { gids, _ := gp.rh.JSONGetString(ctx.CallBy.Accid.Hex(), "$.party.id") if len(gids) == 0 || len(gids[0]) == 0 { return } // mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 gidstr := gids[0] gid, _ := primitive.ObjectIDFromHex(gidstr) mid := ctx.CallBy.Accid tid := ctx.Arguments[0].(string) gd := partyDoc{ id: gid, rh: gp.rh, } var err error if len(tid) > 0 { if tid != gd.tid(mid) { // mid가 incharge여야 한다. 그래야 tid를 쫓아낼 수 있음 incharge, err := gp.rh.JSONGet(gd.strid(), "$._incharge") if err != nil { logger.Println("LeaveParty failed. gp.rh.JSONGet returns err :", err) return } if !strings.Contains(incharge.(string), gd.tid(mid)) { // incharge가 아니네? logger.Println("LeaveParty failed. mid is not incharge") return } mid = midFromTid(gd.id, tid) } err = gd.removeMemberByTid(tid) } else { err = gd.removeMember(mid) // 내가 나갔다 gp.rh.JSONDel(mid.Hex(), "$.party.id") } if err != nil { logger.Println("LeaveParty failed. gd.removeMember returns err :", err) return } if gd.getIncharge() == gd.tid(mid) { // 방장이 나감. 방 폭파 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gidstr, Body: bson.M{"gid": gid}, Tag: []string{"GroupDocFull", gidstr}, }) gd.rh.Del(gd.rh.Context(), gd.strid()).Result() } else { // mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: mid.Hex(), Body: bson.M{"gid": gid}, Tag: []string{"GroupDocFull", gid.Hex()}, }) // gid에는 제거 메시지 보냄 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gd.strid(), Body: bson.M{ tid: bson.M{}, }, Tag: []string{"MemberDocFull"}, }) } gp.leaveRoom(gid, mid) } func (gp *groupParty) DenyPartyInvitation(ctx wshandler.ApiCallContext) { gid, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) mid := ctx.CallBy.Accid gp.rh.Del(context.Background(), "inv."+mid.Hex()).Result() gd := partyDoc{ id: gid, rh: gp.rh, } gd.removeMember(mid) gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gd.strid(), Body: bson.M{ gd.tid(mid): bson.M{}, }, Tag: []string{"MemberDocFull"}, }) }