From d7d7df4a28cbed18b27c1e27a5c72b0634b05b6b Mon Sep 17 00:00:00 2001 From: mountain Date: Fri, 6 Oct 2023 11:13:03 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9D=B8=EC=8A=A4=ED=84=B4=ED=8A=B8=20?= =?UTF-8?q?=EA=B7=B8=EB=A3=B9=20=EC=B6=94=EA=B0=80(=EB=9E=9C=EB=8D=A4?= =?UTF-8?q?=EB=A7=A4=EC=B9=AD=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/group_instant.go | 396 ++++++++++++++++++++++++++++++++++++++++++ core/group_party.go | 55 +++--- core/tavern.go | 7 + core/tavern_test.go | 2 +- 4 files changed, 431 insertions(+), 29 deletions(-) create mode 100644 core/group_instant.go diff --git a/core/group_instant.go b/core/group_instant.go new file mode 100644 index 0000000..7cd3bda --- /dev/null +++ b/core/group_instant.go @@ -0,0 +1,396 @@ +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.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() + } + } +} diff --git a/core/group_party.go b/core/group_party.go index 83fed47..7f71d1d 100644 --- a/core/group_party.go +++ b/core/group_party.go @@ -9,6 +9,7 @@ import ( "time" "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" @@ -54,7 +55,7 @@ type memberDoc struct { type InvitationFail bson.M -type groupDoc struct { +type partyDoc struct { Members map[string]any `json:"_members"` InCharge string `json:"_incharge"` Gid string `json:"_gid"` @@ -63,7 +64,7 @@ type groupDoc struct { id groupID } -func (gd *groupDoc) loadMemberFull(tid string) (bson.M, error) { +func (gd *partyDoc) loadMemberFull(tid string) (bson.M, error) { full, err := gd.rh.JSONGet(gd.strid(), "$._members."+tid) if err != nil { return nil, err @@ -80,7 +81,7 @@ func (gd *groupDoc) loadMemberFull(tid string) (bson.M, error) { return doc, nil } -func (gd *groupDoc) loadFull() (doc bson.M) { +func (gd *partyDoc) loadFull() (doc bson.M) { // 새 멤버에 그룹 전체를 알림 full, err := gd.rh.JSONGet(gd.strid(), "$") if err == nil { @@ -96,18 +97,18 @@ func (gd *groupDoc) loadFull() (doc bson.M) { return } -func (gd *groupDoc) strid() string { +func (gd *partyDoc) strid() string { if len(gd.Gid) == 0 { gd.Gid = gd.id.Hex() } return gd.Gid } -func (gd *groupDoc) tid(in accountID) string { +func (gd *partyDoc) tid(in accountID) string { return makeTid(gd.id, in) } -func (gd *groupDoc) mid(tid string) accountID { +func (gd *partyDoc) mid(tid string) accountID { tidobj, _ := primitive.ObjectIDFromHex(tid) var out primitive.ObjectID for i := range tidobj { @@ -116,7 +117,7 @@ func (gd *groupDoc) mid(tid string) accountID { return out } -func (gd *groupDoc) addInvite(mid accountID, body bson.M, ttl time.Duration, max int) (*memberDoc, error) { +func (gd *partyDoc) addInvite(mid accountID, body bson.M, ttl time.Duration, max int) (*memberDoc, error) { targetmid := mid targetbody := body @@ -170,7 +171,7 @@ func (gd *groupDoc) addInvite(mid accountID, body bson.M, ttl time.Duration, max return newdoc, err } -func (gd *groupDoc) addMember(mid accountID, character bson.M) (bson.M, error) { +func (gd *partyDoc) addMember(mid accountID, character bson.M) (bson.M, error) { tid := gd.tid(mid) prefix := "$._members." + tid @@ -186,7 +187,7 @@ func (gd *groupDoc) addMember(mid accountID, character bson.M) (bson.M, error) { return gd.loadMemberFull(tid) } -func (gd *groupDoc) removeMemberByTid(tid string) error { +func (gd *partyDoc) removeMemberByTid(tid string) error { _, err := gd.rh.JSONDel(gd.strid(), "$._members."+tid) if err != nil { return err @@ -204,11 +205,11 @@ func (gd *groupDoc) removeMemberByTid(tid string) error { return err } -func (gd *groupDoc) removeMember(mid accountID) error { +func (gd *partyDoc) removeMember(mid accountID) error { return gd.removeMemberByTid(gd.tid(mid)) } -func (gd *groupDoc) getMembers() (map[string]any, error) { +func (gd *partyDoc) getMembers() (map[string]any, error) { res, err := gd.rh.JSONGet(gd.strid(), "$._members") if err != nil { return nil, err @@ -486,7 +487,7 @@ func (gp *groupParty) AcceptPartyInvitation(w http.ResponseWriter, r *http.Reque // 기존에 이미 파티에 들어가 있다. // 기존 파티에서는 탈퇴 oldgid, _ := primitive.ObjectIDFromHex(pids[0]) - oldgd := &groupDoc{ + oldgd := &partyDoc{ id: oldgid, rh: gp.rh, } @@ -503,7 +504,7 @@ func (gp *groupParty) AcceptPartyInvitation(w http.ResponseWriter, r *http.Reque gp.leaveRoom(oldgid, mid) } - gd := &groupDoc{ + gd := &partyDoc{ id: gid, rh: gp.rh, } @@ -557,7 +558,7 @@ func (gp *groupParty) QueryPartyMemberState(w http.ResponseWriter, r *http.Reque } func (gp *groupParty) updateMemberDocument(gid groupID, mid accountID, doc bson.M) error { - gd := &groupDoc{ + gd := &partyDoc{ id: gid, rh: gp.rh, } @@ -582,7 +583,7 @@ func (gp *groupParty) updateMemberDocument(gid groupID, mid accountID, doc bson. } func (gp *groupParty) updatePartyDocument(gid groupID, frag bson.M) error { - gd := groupDoc{ + gd := partyDoc{ id: gid, rh: gp.rh, } @@ -628,7 +629,7 @@ func (gp *groupParty) QueryPartyMembers(w http.ResponseWriter, r *http.Request) return } - gd := groupDoc{ + gd := partyDoc{ id: gid, rh: gp.rh, } @@ -647,10 +648,10 @@ func (gp *groupParty) QueryPartyMembers(w http.ResponseWriter, r *http.Request) } } -func (gp *groupParty) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*groupDoc, error) { +func (gp *groupParty) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*partyDoc, error) { tid := makeTid(newid, charge) - gd := &groupDoc{ + gd := &partyDoc{ Members: map[string]any{ tid: &memberDoc{ Body: chargeDoc, @@ -671,7 +672,7 @@ func (gp *groupParty) createGroup(newid groupID, charge accountID, chargeDoc bso return gd, nil } -func (gp *groupParty) find(id groupID) (*groupDoc, error) { +func (gp *groupParty) find(id groupID) (*partyDoc, error) { if id.IsZero() { return nil, nil } @@ -684,14 +685,13 @@ func (gp *groupParty) find(id groupID) (*groupDoc, error) { return nil, err } - return &groupDoc{ + return &partyDoc{ rh: gp.rh, id: id, }, nil } - -func (gp *groupParty) ClientDisconnected(ctx wshandler.ApiCallContext) { - gids, _ := gp.rh.JSONGetString(ctx.CallBy.Accid.Hex(), "$.party.id") +func (gp *groupParty) ClientDisconnected(conn *websocket.Conn, callby *wshandler.Sender) { + gids, _ := gp.rh.JSONGetString(callby.Accid.Hex(), "$.party.id") if len(gids) > 0 && len(gids[0]) > 0 { // mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 @@ -699,18 +699,17 @@ func (gp *groupParty) ClientDisconnected(ctx wshandler.ApiCallContext) { gid, _ := primitive.ObjectIDFromHex(gidstr) // 나를 먼저 룸에서 빼야 나한테 메시지가 안감 - gp.leaveRoom(gid, ctx.CallBy.Accid) + gp.leaveRoom(gid, callby.Accid) // gid에는 제거 메시지 보냄 gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gidstr, Body: bson.M{ - makeTid(gid, ctx.CallBy.Accid): bson.M{}, + makeTid(gid, callby.Accid): bson.M{}, }, Tag: []string{"MemberDocFull"}, }) - } } @@ -740,7 +739,7 @@ func (gp *groupParty) LeaveParty(ctx wshandler.ApiCallContext) { mid := ctx.CallBy.Accid tid := ctx.Arguments[0].(string) - gd := groupDoc{ + gd := partyDoc{ id: gid, rh: gp.rh, } @@ -797,7 +796,7 @@ func (gp *groupParty) DenyPartyInvitation(ctx wshandler.ApiCallContext) { mid := ctx.CallBy.Accid gp.rh.Del(context.Background(), "inv."+mid.Hex()).Result() - gd := groupDoc{ + gd := partyDoc{ id: gid, rh: gp.rh, } diff --git a/core/tavern.go b/core/tavern.go index e9cd8ad..4a88508 100644 --- a/core/tavern.go +++ b/core/tavern.go @@ -109,6 +109,13 @@ func (tv *Tavern) prepare(ctx context.Context) error { tv.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(party, "party")) } + instant := new(groupInstant) + if err := instant.Initialize(tv); err != nil { + return logger.ErrorWithCallStack(err) + } + tv.httpApiBorker.AddHandler(gocommon.MakeHttpApiHandler(instant, "instant")) + tv.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(instant, "instant")) + return nil } diff --git a/core/tavern_test.go b/core/tavern_test.go index 2401990..7036a6b 100644 --- a/core/tavern_test.go +++ b/core/tavern_test.go @@ -84,7 +84,7 @@ func TestReJSON(t *testing.T) { }, } - gd := groupDoc{ + gd := partyDoc{ id: primitive.NewObjectID(), }