From 310397dd2b854beb9e7afbc54b55f91eedbefbb6 Mon Sep 17 00:00:00 2001 From: mountain Date: Tue, 25 Jul 2023 18:11:02 +0900 Subject: [PATCH] =?UTF-8?q?party=EB=A1=9C=20=EA=B7=B8=EB=A3=B9=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config_template.json | 54 +-- core/apiimpl.go | 526 ------------------------------ core/group.go | 76 +++-- core/group_chat.go | 22 ++ core/group_memory.go | 577 -------------------------------- core/group_party.go | 759 +++++++++++++++++++++++++++++++++++++++++++ core/tavern.go | 129 +++----- 7 files changed, 881 insertions(+), 1262 deletions(-) delete mode 100644 core/apiimpl.go create mode 100644 core/group_chat.go delete mode 100644 core/group_memory.go create mode 100644 core/group_party.go diff --git a/config_template.json b/config_template.json index ef23c98..602ecc9 100644 --- a/config_template.json +++ b/config_template.json @@ -13,11 +13,11 @@ "dev": { "mongo": "mongodb://192.168.8.94:27017/?replicaSet=repl01&retrywrites=false", "redis": { - "cache": "redis://192.168.8.94:6380/4", - "session": "redis://192.168.8.94:6380/5", - "ranking": "redis://192.168.8.94:6380/6", - "wshandler": "redis://192.168.8.94:6380/7", - "tavern": "redis://192.168.8.94:6380/8" + "cache": "redis://192.168.8.94:6380/5", + "session": "redis://192.168.8.94:6380/6", + "ranking": "redis://192.168.8.94:6380/7", + "wshandler": "redis://192.168.8.94:6380/8", + "tavern": "redis://192.168.8.94:6380/9" } } }, @@ -26,48 +26,12 @@ "maingate_api_token": "63d08aa34f0162622c11284b", "tavern_service_url": "http://localhost/tavern", "tavern_group_types": { - "subjugate": { - "text_search_field": [ - "name" - ], - "unique_index": [ - "name,_id", - "_id,members", - "name,hidden" - ], - "search_index": [ - "rules" - ], - "member_index": [ - "_gid,candidate,luts", - "_gid,luts", - "_gid,expiring" - ], - "invite_ttl": 30, - "candidate_ttl": 3600, - "invitee_exlusive": true, - "invitee_is_member": true, - "max_member": 4 - }, - "lobby": { + "party": { "max_member": 3, - "invitee_exlusive": true, - "invitee_is_member": true, - "transient": true, "invite_ttl": 30 - } - }, - - "services": { - "kingdom": { - "개발중": { - "url": "http://localhost/warehouse/dev", - "development": true - }, - "개인서버": { - "url": "http://localhost/warehouse/private", - "development": false - } + }, + "chat" : { + "transient" : true } } } \ No newline at end of file diff --git a/core/apiimpl.go b/core/apiimpl.go deleted file mode 100644 index b784438..0000000 --- a/core/apiimpl.go +++ /dev/null @@ -1,526 +0,0 @@ -package core - -import ( - "net/http" - - common "repositories.action2quare.com/ayo/gocommon" - "repositories.action2quare.com/ayo/gocommon/logger" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -// CreateGroup : 그룹 생성 -// - 그룹 : 멤버와 권한을 관리할 수 있다. 그룹 타입에 따라 디비에 저장되거나 메모리에만 존재한다. -// - 생성 요청이 오면 파티를 만든다. 파티을 만들 수 있는지 여부는 서비스에서 결정할 것이고, 이 요청을 호출했다는 것은 서비스가 결정한 그룹 생성 조건을 다 통과했다는 의미이다. -// - parameter : -// - type : 그룹 종류. 그룹 종류에 따라 인덱스와 쿼리 가능 field가 다르다. -func (sub *subTavern) CreateGroup(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - grouptype := sub.groups[typename] - if grouptype == nil { - logger.Println("CreateGroup failed. group type is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - - doc := bson.M{} - if err := readBsonDoc(r.Body, &doc); err != nil { - logger.Error("CreateGroup failed. readBsonDoc returns err :", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - inserted, err := grouptype.Create(r.Form, doc) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - - w.Write(inserted[:]) -} - -// JoinGroup : 그룹에 참가 -// - type : 그룹 타입 -// - 그룹 타입에 맞는 키(주로 _id) -// - member_id : 참가 멤버의 아이디 -// - body : 멤버의 속성 bson document -func (sub *subTavern) JoinGroup(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("JoinGroup failed. group type is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - - doc := bson.M{} - if err := readBsonDoc(r.Body, &doc); err != nil { - logger.Error("JoinGroup failed. readBsonDoc returns err :", err) - w.WriteHeader(http.StatusBadRequest) - return - } - gidobj, ok := common.ReadObjectIDFormValue(r.Form, "gid") - if !ok { - logger.Println("JoinGroup failed. gid is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - midobj, midok := common.ReadObjectIDFormValue(r.Form, "mid") - tidobj, tidok := common.ReadObjectIDFormValue(r.Form, "tid") - if !midok && !tidok { - // 둘다 없네? - logger.Println("JoinGroup failed. tid or mid should be exist") - w.WriteHeader(http.StatusBadRequest) - return - } - - var err error - if candidate, ok := common.ReadBoolFormValue(r.Form, "candidate"); ok && candidate { - err = group.Candidate(gidobj, midobj, doc) - } else { - err = group.Join(gidobj, midobj, doc) - } - - if err == nil { - writeBsonDoc(w, map[string]string{ - "gid": gidobj.Hex(), - "tid": tidobj.Hex(), - }) - } else if err == errGroupNotExist { - w.Write([]byte("{}")) - } else if err != nil { - logger.Error("JoinGroup failed :", err) - w.WriteHeader(http.StatusInternalServerError) - } -} - -// Invite : 초대 -// - type : 초대 타입 (required) -// - from : 초대하는 자 (required) -// - to : 초대받는 자 (required) -// - timeout : 초대 유지시간(optional. 없으면 config 기본 값) -// - (body) : 검색시 노출되는 document -func (sub *subTavern) Invite(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("Invite failed. group type is missing :", r) - w.WriteHeader(http.StatusBadRequest) - return - } - gid, ok := common.ReadObjectIDFormValue(r.Form, "gid") - if !ok { - logger.Println("Invite failed. gid is missing :", r) - w.WriteHeader(http.StatusBadRequest) - return - } - mid, ok := common.ReadObjectIDFormValue(r.Form, "mid") - if !ok { - logger.Println("Invite failed. mid is missing :", r) - w.WriteHeader(http.StatusBadRequest) - return - } - - var reqdoc struct { - Inviter bson.M `bson:"inviter"` - Invitee bson.M `bson:"invitee"` - } - if err := readBsonDoc(r.Body, &reqdoc); err != nil { - logger.Println("Invite failed. readBsonDoc returns err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - result, err := group.Invite(gid, mid, reqdoc.Inviter, reqdoc.Invitee) - if err != nil { - logger.Println("Invite failed. group.Invite returns err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write([]byte(result)) -} - -func (sub *subTavern) CancelInvitation(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("CancelInvitation failed. group type is missing :", r) - w.WriteHeader(http.StatusBadRequest) - return - } - tid, ok := common.ReadObjectIDFormValue(r.Form, "tid") - if !ok { - logger.Println("CancelInvitation failed. form value 'tid' is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - gid, ok := common.ReadObjectIDFormValue(r.Form, "gid") - if !ok { - logger.Println("CancelInvitation failed. form value 'gid' is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - if err := group.CancelInvitation(gid, tid); err != nil { - logger.Println("CancelInvitation failed. group.CancelInvitation returns err :", err) - w.WriteHeader(http.StatusInternalServerError) - } -} - -func (sub *subTavern) AcceptInvitation(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("CancelInvitation failed. group type is missing :", r) - w.WriteHeader(http.StatusBadRequest) - return - } - - gid, _ := common.ReadObjectIDFormValue(r.Form, "gid") - mid, _ := common.ReadObjectIDFormValue(r.Form, "mid") - - var member bson.M - if err := readBsonDoc(r.Body, &member); err != nil { - logger.Error("AcceptInvitation failed. readBsonDoc returns err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - err := group.AcceptInvitation(gid, mid, member) - if err != nil { - logger.Println("AcceptInvitation failed. group.AcceptInvitation returns err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (sub *subTavern) DenyInvitation(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("DenyInvitation failed. group type is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - gid, _ := common.ReadObjectIDFormValue(r.Form, "gid") - mid, _ := common.ReadObjectIDFormValue(r.Form, "mid") - tid, ok := common.ReadObjectIDFormValue(r.Form, "tid") - if !ok { - logger.Println("DenyInvitation failed. tid is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - err := group.DenyInvitation(gid, mid, tid) - if err != nil { - logger.Error("DenyInvitation failed. group.DenyInvitation returns err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (sub *subTavern) QueryInvitations(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("QueryInvitations failed. group type is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - - mid, ok := common.ReadObjectIDFormValue(r.Form, "mid") - if !ok { - logger.Println("QueryInvitations failed. mid is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - - var after primitive.Timestamp - if v, ok := common.ReadStringFormValue(r.Form, "after"); ok && v != "0.0" { - after = common.DotStringToTimestamp(v) - } - - result, err := group.QueryInvitations(mid, after) - - if err != nil { - logger.Println("QueryInvitations failed. group.QueryInvitations returns err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if err := writeBsonArr(w, result); err != nil { - logger.Println("QueryInvitations failed. Encode returns err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (sub *subTavern) SearchGroup(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("SearchGroup failed. group type is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - - projection, _ := common.ReadStringFormValue(r.Form, "projection") - - var filter bson.M - if err := readBsonDoc(r.Body, &filter); err != nil { - logger.Error("SearchGroup failed. readBsonDoc returns err :", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - result, err := group.FindAll(filter, projection, primitive.Timestamp{}) - if err != nil { - logger.Error("SearchGroup failed. FindAll err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if result == nil { - return - } - - if err := writeBsonArr(w, result); err != nil { - logger.Error("bson marshal failed :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (sub *subTavern) QueryOnlineState(w http.ResponseWriter, r *http.Request) { - mid, ok := common.ReadObjectIDFormValue(r.Form, "mid") - if !ok { - logger.Println("IsOnline failed. mid is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - - state := sub.wsh.GetState(sub.region, mid) - w.Write([]byte(state)) -} - -func (sub *subTavern) IsOnline(w http.ResponseWriter, r *http.Request) { - mid, ok := common.ReadObjectIDFormValue(r.Form, "mid") - if !ok { - logger.Println("IsOnline failed. mid is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - - if state := sub.wsh.GetState(sub.region, mid); len(state) > 0 { - w.Write([]byte("true")) - } else { - w.Write([]byte("false")) - } -} - -// QueryGroup : 그룹조회 -// - type : 그룹 타입 -// - 그룹 타입에 맞는 키(주로 _id) -// - projection : select할 필드. ,로 구분 -func (sub *subTavern) QueryGroup(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("QueryGroup failed. group type is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - - gid, ok := common.ReadObjectIDFormValue(r.Form, "_id") - if !ok { - logger.Println("QueryGroup failed. _id is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - projection, _ := common.ReadStringFormValue(r.Form, "projection") - - after, _ := common.ReadStringFormValue(r.Form, "after") - if after != "0.0" { - projection += ",+luts" - } - - result, err := group.FindOne(gid, projection) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - if result == nil { - return - } - if len(after) > 0 { - if luts, ok := result["luts"].(primitive.Timestamp); ok { - afterts := common.DotStringToTimestamp(after) - if primitive.CompareTimestamp(luts, afterts) < 0 { - return - } - } - } - - if err := writeBsonDoc(w, result); err != nil { - logger.Error("bson marshal failed :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -// LeaveGroup : 그룹에서 나감 or 내보냄 -// - type : 그룹 타입 -// - 그룹 타입에 맞는 키(주로 _id) -// - member_id : 나갈 멤버의 아이디 -func (sub *subTavern) LeaveGroup(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("LeaveGroup failed. group type is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - - gid, ok := common.ReadObjectIDFormValue(r.Form, "gid") - if !ok { - logger.Println("LeaveGroup failed. gid is missing :", r.Form) - w.WriteHeader(http.StatusBadRequest) - return - } - mid, midok := common.ReadObjectIDFormValue(r.Form, "mid") - - if !midok { - logger.Println("LeaveGroup failed. mid is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - if err := group.Leave(gid, mid); err != nil { - // 둘 중 하나는 있어야지 - logger.Println("LeaveGroup failed. group.Leave returns err :", err) - w.WriteHeader(http.StatusBadRequest) - } -} - -func (sub *subTavern) UpdateMemberDocument(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("DismissGroup failed. type is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - midobj, ok := common.ReadObjectIDFormValue(r.Form, "mid") - if !ok { - logger.Println("UpdateMemberDocument failed. member_id is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - gidobj, ok := common.ReadObjectIDFormValue(r.Form, "gid") - if !ok { - logger.Println("UpdateMemberDocument failed. _id is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - var updatedoc bson.M - if err := readBsonDoc(r.Body, &updatedoc); err != nil { - logger.Error("UpdateMemberDocument failed. body decoding error :", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - if err := group.UpdateMemberDocument(gidobj, midobj, updatedoc); err != nil { - logger.Println("UpdateMemberDocument failed :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (sub *subTavern) DismissGroup(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("DismissGroup failed. type is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - gid, ok := common.ReadObjectIDFormValue(r.Form, "gid") - if !ok { - logger.Println("DismissGroup failed. gid is missing :") - w.WriteHeader(http.StatusBadRequest) - return - } - - if err := group.Dismiss(gid); err != nil { - logger.Error("DismissGroup failed. group.Dismiss returns err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (sub *subTavern) UpdateGroupDocument(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("UpdateGroupDocument failed. type is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - gid, ok := common.ReadObjectIDFormValue(r.Form, "gid") - if !ok { - logger.Println("UpdateGroupDocument failed. gid is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - var frag bson.M - if err := readBsonDoc(r.Body, &frag); err != nil { - logger.Error("UpdateGroupDocument failed. readBsonDoc err :", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - if err := group.UpdateGroupDocument(gid, frag); err != nil { - logger.Error("UpdateGroupDocument failed. group.UpdateGroupDocument returns err :", err) - w.WriteHeader(http.StatusBadRequest) - return - } -} - -func (sub *subTavern) QueryGroupMembers(w http.ResponseWriter, r *http.Request) { - typename, _ := common.ReadStringFormValue(r.Form, "type") - group := sub.groups[typename] - if group == nil { - logger.Println("QueryGroupMembers failed. type is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - gid, ok := common.ReadObjectIDFormValue(r.Form, "gid") - if !ok { - logger.Println("QueryGroupMembers failed. gid is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - members, err := group.QueryGroupMembers(gid) - if err != nil { - logger.Error("QueryGroupMembers failed. group.QueryGroupMembers returns err :", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - if err := writeBsonDoc(w, members); err != nil { - logger.Error("QueryGroupMembers failed. writeBsonDoc return err :", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/core/group.go b/core/group.go index a82550a..6de00eb 100644 --- a/core/group.go +++ b/core/group.go @@ -1,43 +1,55 @@ package core import ( - "net/url" + "net/http" + "reflect" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" + "repositories.action2quare.com/ayo/gocommon/wshandler" ) -type groupConfig struct { - UniqueIndex []string `json:"unique_index"` - SearchIndex []string `json:"search_index"` - MemberIndex []string `json:"member_index"` - TextSearchFields []string `json:"text_search_field"` - InviteExpire int32 `json:"invite_ttl"` // 그룹이 개인에게 보낸 초대장 만료 기한 - CandidateExpire int32 `json:"candidate_ttl"` // 개인이 그룹에게 보낸 신청서 만료 기한 - InviteeExlusive bool `json:"invitee_exlusive"` - InviteeIsMember bool `json:"invitee_is_member"` - MaxMember int `json:"max_member"` - Transient bool `json:"transient"` +var groupTypes map[string]reflect.Type - Name string +func groupTypeContainer() map[string]reflect.Type { + if groupTypes == nil { + groupTypes = make(map[string]reflect.Type) + } + return groupTypes } +type apiFuncType func(http.ResponseWriter, *http.Request) +type apiFuncsContainer struct { + normfuncs map[string]apiFuncType + funcs map[string][]apiFuncType +} + +func (afc *apiFuncsContainer) registApiFunction(name string, f apiFuncType) { + afc.funcs[name] = append(afc.funcs[name], f) +} + +func (afc *apiFuncsContainer) normalize() { + for k, v := range afc.funcs { + if len(v) == 1 { + afc.normfuncs[k] = v[0] + } else { + afc.normfuncs[k] = func(w http.ResponseWriter, r *http.Request) { + for _, f := range v { + f(w, r) + } + } + } + } + afc.funcs = nil +} + +func (afc *apiFuncsContainer) call(fn string, w http.ResponseWriter, r *http.Request) { + f := afc.normfuncs[fn] + if f != nil { + f(w, r) + } +} + +type configDocument map[string]any type group interface { - Create(form url.Values, doc bson.M) (primitive.ObjectID, error) - Candidate(groupID primitive.ObjectID, memberID primitive.ObjectID, doc bson.M) error - Join(groupID primitive.ObjectID, memberID primitive.ObjectID, doc bson.M) error - Invite(groupID primitive.ObjectID, memberID primitive.ObjectID, inviterDoc bson.M, inviteeDoc bson.M) (string, error) - CancelInvitation(groupID primitive.ObjectID, ticketID primitive.ObjectID) error - AcceptInvitation(groupID primitive.ObjectID, mid primitive.ObjectID, member bson.M) error - DenyInvitation(groupID primitive.ObjectID, mid primitive.ObjectID, ticketID primitive.ObjectID) error - QueryInvitations(memberID primitive.ObjectID, after primitive.Timestamp) ([]bson.M, error) - Exist(groupID primitive.ObjectID, filter bson.M) (bool, error) - FindAll(filter bson.M, projection string, after primitive.Timestamp) ([]bson.M, error) - FindOne(groupID primitive.ObjectID, projection string) (bson.M, error) - Leave(groupID primitive.ObjectID, memberID primitive.ObjectID) error - UpdateMemberDocument(groupID primitive.ObjectID, memberID primitive.ObjectID, doc bson.M) error - Dismiss(groupID primitive.ObjectID) error - UpdateGroupDocument(groupID primitive.ObjectID, doc bson.M) error - QueryGroupMembers(groupID primitive.ObjectID) (bson.M, error) - MemberDisconnected(room string, mid primitive.ObjectID) + Initialize(*subTavern, configDocument) error + ClientMessageReceved(*wshandler.Sender, wshandler.WebSocketMessageType, any) } diff --git a/core/group_chat.go b/core/group_chat.go new file mode 100644 index 0000000..d938bbe --- /dev/null +++ b/core/group_chat.go @@ -0,0 +1,22 @@ +package core + +import ( + "reflect" + + "repositories.action2quare.com/ayo/gocommon/wshandler" +) + +func init() { + groupTypeContainer()["chat"] = reflect.TypeOf(&groupChat{}) +} + +type groupChat struct { +} + +func (gc *groupChat) Initialize(*subTavern, configDocument) error { + return nil +} + +func (gc *groupChat) ClientMessageReceved(*wshandler.Sender, wshandler.WebSocketMessageType, any) { + +} diff --git a/core/group_memory.go b/core/group_memory.go deleted file mode 100644 index 4c34fb1..0000000 --- a/core/group_memory.go +++ /dev/null @@ -1,577 +0,0 @@ -package core - -import ( - "context" - "encoding/gob" - "encoding/json" - "errors" - "fmt" - "net/url" - "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 ticketID = primitive.ObjectID -type groupID = primitive.ObjectID - -func init() { - gob.Register(memberDoc{}) - gob.Register(groupDoc{}) - gob.Register(Invitation{}) - gob.Register(InvitationFail{}) -} - -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() -} - -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 groupDoc struct { - Members map[string]any `json:"_members"` - InCharge string `json:"_incharge"` - Gid string `json:"_gid"` - - rh *gocommon.RedisonHandler - id groupID -} - -func (gd *groupDoc) 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 *groupDoc) 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 *groupDoc) strid() string { - if len(gd.Gid) == 0 { - gd.Gid = gd.id.Hex() - } - return gd.Gid -} - -func (gd *groupDoc) tid(in accountID) string { - return makeTid(gd.id, in) -} - -func (gd *groupDoc) 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 *groupDoc) addInvite(inviteeDoc bson.M, ttl time.Duration, max int) (*memberDoc, error) { - targetmid := inviteeDoc["_mid"].(accountID) - targetbody := inviteeDoc["body"].(bson.M) - - // 초대 가능한 빈 자리가 있나 - 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 *groupDoc) addMember(mid accountID, doc bson.M) (bson.M, error) { - tid := gd.tid(mid) - prefix := "$._members." + tid - - if _, err := gd.rh.JSONMerge(gd.strid(), prefix+"._body", doc, 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 - } - - return gd.loadMemberFull(tid) -} - -func (gd *groupDoc) removeMember(mid accountID) error { - _, err := gd.rh.JSONDel(gd.strid(), "$._members."+gd.tid(mid)) - return err -} - -func (gd *groupDoc) 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 groupInMemory struct { - *groupConfig - sendUpstreamMessage func(*wshandler.UpstreamMessage) - sendEnterRoomMessage func(groupID, accountID) - sendLeaveRoomMessage func(groupID, accountID) - rh *gocommon.RedisonHandler -} - -func (gm *groupInMemory) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*groupDoc, error) { - tid := makeTid(newid, charge) - - gd := &groupDoc{ - Members: map[string]any{ - tid: &memberDoc{ - Body: chargeDoc, - Invite: false, - InviteExpire: 0, - }, - }, - InCharge: tid, - - rh: gm.rh, - id: newid, - } - - _, err := gm.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX) - if err != nil { - return nil, err - } - return gd, nil -} - -func (gm *groupInMemory) find(id groupID) (*groupDoc, error) { - if id.IsZero() { - return nil, nil - } - - _, err := gm.rh.JSONObjLen(id.Hex(), "$") - if err == redis.Nil { - return nil, nil - } - if err != nil { - return nil, err - } - - return &groupDoc{ - rh: gm.rh, - id: id, - }, nil -} - -func (gm *groupInMemory) Create(form url.Values, base bson.M) (groupID, error) { - return primitive.NilObjectID, nil -} - -func (gm *groupInMemory) Candidate(gid groupID, mid accountID, doc bson.M) error { - logger.Error("not implemented func : Canidate") - return nil -} - -var errGroupNotExist = errors.New("group does not exist") - -func (gm *groupInMemory) Join(gid groupID, mid accountID, doc bson.M) error { - gd, err := gm.find(gid) - if err != nil { - return err - } - - if gd == nil { - // 그룹이 없다. 실패 - return errGroupNotExist - } - - // 내 정보 업데이트할 때에도 사용됨 - if memdoc, err := gd.addMember(mid, doc); err == nil { - // 기존 유저에게 새 유저 알림 - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "#" + gid.Hex(), - Body: map[string]any{ - gd.tid(mid): memdoc, - }, - Tag: []string{"MemberDocFull"}, - }) - - gm.sendEnterRoomMessage(gid, mid) - - // 새 멤버에 그룹 전체를 알림 - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "@" + mid.Hex(), - Body: gd.loadFull(), - Tag: []string{"GroupDocFull"}, - }) - } - - return err -} - -var errInviteeDocMidMissing = errors.New("inviteeDoc must have '_mid' field") -var errAlreadyInvited = errors.New("this target is already invited by someone or me") - -func (gm *groupInMemory) Invite(gid groupID, mid accountID, inviterDoc bson.M, inviteeDoc bson.M) (string, error) { - targetid, ok := inviteeDoc["_mid"].(accountID) - if !ok { - return "", errInviteeDocMidMissing - } - - // targetid에 초대한 mid가 들어있다. - already, err := gm.rh.Get(context.Background(), targetid.Hex()).Result() - if err != nil && err != redis.Nil { - return "", err - } - - if len(already) > 0 { - if already != mid.Hex() { - // 이미 초대 중이다. - // inviter한테 알려줘야 한다. - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "@" + mid.Hex(), - Body: inviteeDoc, - Tag: []string{"InvitationFail"}, - }) - } - return "", errAlreadyInvited - } - - gd, err := gm.find(gid) - if err != nil { - return "", err - } - - if gd == nil { - gd, err = gm.createGroup(gid, mid, inviterDoc) - if err != nil { - return "", err - } - // 내가 wshandler room에 입장 - gm.sendEnterRoomMessage(gid, mid) - - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "@" + mid.Hex(), - Body: gd, - Tag: []string{"GroupDocFull"}, - }) - } - - newdoc, err := gd.addInvite(inviteeDoc, time.Duration(gm.InviteExpire+1)*time.Second, gm.MaxMember) - if err != nil { - return "", err - } - - // 초대 중 표시 - _, err = gm.rh.SetNX(context.Background(), targetid.Hex(), mid.Hex(), time.Duration(gm.InviteExpire)*time.Second).Result() - if err != nil { - return "", err - } - - // invitee에게 알림 - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "@" + targetid.Hex(), - Body: Invitation{ - GroupID: gid, - TicketID: gd.tid(targetid), - Inviter: inviterDoc, - ExpireAtUTC: newdoc.InviteExpire, - }, - Tag: []string{"Invitation"}, - }) - - return gd.strid(), nil -} - -func (gm *groupInMemory) CancelInvitation(gid groupID, tid ticketID) error { - return nil -} - -var errInvitationExpired = errors.New("invitation is already expired") - -func (gm *groupInMemory) AcceptInvitation(gid groupID, mid accountID, member bson.M) error { - cnt, err := gm.rh.Del(context.Background(), mid.Hex()).Result() - if err != nil { - return err - } - if cnt == 0 { - // 만료됨 - return errInvitationExpired - } - - gd := &groupDoc{ - id: gid, - rh: gm.rh, - } - - memberDoc, err := gd.addMember(mid, member) - if err == nil { - // 기존 멤버에게 새 멤버를 알림 - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "#" + gid.Hex(), - Body: map[string]any{ - gd.tid(mid): memberDoc, - }, - Tag: []string{"MemberDocFull"}, - }) - - gm.sendEnterRoomMessage(gid, mid) - - // 새 멤버에 그룹 전체를 알림 - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "@" + mid.Hex(), - Body: gd.loadFull(), - Tag: []string{"GroupDocFull"}, - }) - } - - // 실패 - return err -} - -func (gm *groupInMemory) QueryGroupMembers(gid groupID) (bson.M, error) { - gd := groupDoc{ - id: gid, - rh: gm.rh, - } - return gd.getMembers() -} - -func (gm *groupInMemory) DenyInvitation(gid groupID, mid accountID, tid ticketID) error { - gm.rh.Del(context.Background(), mid.Hex()).Result() - gd := groupDoc{ - id: gid, - rh: gm.rh, - } - return gd.removeMember(mid) -} - -func (gm *groupInMemory) QueryInvitations(mid accountID, after primitive.Timestamp) ([]bson.M, error) { - return nil, nil -} -func (gm *groupInMemory) Exist(gid groupID, filter bson.M) (bool, error) { - return false, nil -} -func (gm *groupInMemory) FindAll(filter bson.M, projection string, after primitive.Timestamp) ([]bson.M, error) { - return nil, nil -} -func (gm *groupInMemory) FindOne(gid groupID, projection string) (bson.M, error) { - return nil, nil -} -func (gm *groupInMemory) Leave(gid groupID, mid accountID) error { - gd := groupDoc{ - id: gid, - rh: gm.rh, - } - if err := gd.removeMember(mid); err != nil { - return err - } - - // 나한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "@" + mid.Hex(), - Body: bson.M{"gid": gid}, - Tag: []string{"GroupDocFull", gid.Hex()}, - }) - gm.sendLeaveRoomMessage(gid, mid) - - return nil -} - -func (gm *groupInMemory) UpdateMemberDocument(gid groupID, mid accountID, doc bson.M) error { - gd := &groupDoc{ - id: gid, - rh: gm.rh, - } - - prefixPath := fmt.Sprintf("$._members.%s.", gd.tid(mid)) - err := gm.rh.JSONMSetRel(gd.strid(), prefixPath, doc) - if err != nil { - return err - } - - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "#" + gid.Hex(), - Body: map[string]any{ - gd.tid(mid): doc, - }, - Tag: []string{"MemberDocFragment"}, - }) - - return nil -} - -func (gm *groupInMemory) Dismiss(gid groupID) error { - return nil -} - -func (gm *groupInMemory) UpdateGroupDocument(gid groupID, frag bson.M) error { - gd := groupDoc{ - id: gid, - rh: gm.rh, - } - - if err := gm.rh.JSONMSetRel(gd.strid(), "$.", frag); err != nil { - return err - } - - // 업데이트 알림 - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "#" + gid.Hex(), - Body: frag, - Tag: []string{"GroupDocFragment"}, - }) - - return nil -} - -func (gm *groupInMemory) MemberDisconnected(room string, mid primitive.ObjectID) { - gid, err := primitive.ObjectIDFromHex(room) - if err != nil { - return - } - gd := &groupDoc{ - id: gid, - rh: gm.rh, - } - - tid := gd.tid(mid) - deleted, _ := gm.rh.JSONDel(room, "$._members."+tid) - if deleted > 0 { - // 퇴장을 알림 - gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: "#" + room, - Body: bson.M{ - tid: bson.M{}, - }, - Tag: []string{"MemberDocFull"}, - }) - } -} - -func (cfg *groupConfig) prepareInMemory(ctx context.Context, typename string, sub *subTavern) (group, error) { - // group document - // member document - region := sub.region - wsh := sub.wsh - storage := config.RegionStorage[sub.region] - redisClient, err := gocommon.NewRedisClient(storage.Redis["tavern"]) - if err != nil { - return nil, err - } - // 여기서는 subscribe channel - // 각 함수에서는 publish - gm := &groupInMemory{ - groupConfig: cfg, - rh: gocommon.NewRedisonHandler(ctx, redisClient), - sendUpstreamMessage: func(msg *wshandler.UpstreamMessage) { - wsh.SendUpstreamMessage(region, msg) - }, - sendEnterRoomMessage: func(gid groupID, accid accountID) { - wsh.EnterRoom(region, gid.Hex(), accid) - }, - sendLeaveRoomMessage: func(gid groupID, accid accountID) { - wsh.LeaveRoom(region, gid.Hex(), accid) - }, - } - - return gm, nil -} diff --git a/core/group_party.go b/core/group_party.go new file mode 100644 index 0000000..ec493fd --- /dev/null +++ b/core/group_party.go @@ -0,0 +1,759 @@ +package core + +import ( + "context" + "encoding/gob" + "encoding/json" + "fmt" + "net/http" + "reflect" + "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 init() { + gob.Register(memberDoc{}) + gob.Register(groupDoc{}) + gob.Register(Invitation{}) + gob.Register(InvitationFail{}) + + groupTypeContainer()["party"] = reflect.TypeOf(&groupParty{}) +} + +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() +} + +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 groupDoc struct { + Members map[string]any `json:"_members"` + InCharge string `json:"_incharge"` + Gid string `json:"_gid"` + + rh *gocommon.RedisonHandler + id groupID +} + +func (gd *groupDoc) 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 *groupDoc) 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 *groupDoc) strid() string { + if len(gd.Gid) == 0 { + gd.Gid = gd.id.Hex() + } + return gd.Gid +} + +func (gd *groupDoc) tid(in accountID) string { + return makeTid(gd.id, in) +} + +func (gd *groupDoc) 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 *groupDoc) addInvite(inviteeDoc bson.M, ttl time.Duration, max int) (*memberDoc, error) { + targetmid := inviteeDoc["_mid"].(accountID) + targetbody := inviteeDoc["body"].(bson.M) + + // 초대 가능한 빈 자리가 있나 + 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 *groupDoc) addMember(mid accountID, doc bson.M) (bson.M, error) { + tid := gd.tid(mid) + prefix := "$._members." + tid + + if _, err := gd.rh.JSONMerge(gd.strid(), prefix+"._body", doc, 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 + } + + return gd.loadMemberFull(tid) +} + +func (gd *groupDoc) removeMember(mid accountID) error { + _, err := gd.rh.JSONDel(gd.strid(), "$._members."+gd.tid(mid)) + return err +} + +func (gd *groupDoc) 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) + sendEnterRoomMessage func(groupID, accountID) + sendLeaveRoomMessage func(groupID, accountID) + rh *gocommon.RedisonHandler +} + +func (gp *groupParty) Initialize(sub *subTavern, cfg configDocument) error { + rem, _ := json.Marshal(cfg) + err := json.Unmarshal(rem, &gp.partyConfig) + if err != nil { + return err + } + + gp.rh = gocommon.NewRedisonHandler(sub.redisClient.Context(), sub.redisClient) + gp.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) { + sub.wsh.SendUpstreamMessage(sub.region, msg) + } + gp.sendEnterRoomMessage = func(gid groupID, accid accountID) { + sub.wsh.EnterRoom(sub.region, gid.Hex(), accid) + } + gp.sendLeaveRoomMessage = func(gid groupID, accid accountID) { + sub.wsh.LeaveRoom(sub.region, gid.Hex(), accid) + } + + sub.apiFuncs.registApiFunction("JoinParty", gp.JoinParty) + sub.apiFuncs.registApiFunction("InviteToParty", gp.InviteToParty) + sub.apiFuncs.registApiFunction("AcceptPartyInvitation", gp.AcceptPartyInvitation) + sub.apiFuncs.registApiFunction("DenyPartyInvitation", gp.DenyPartyInvitation) + sub.apiFuncs.registApiFunction("QueryPartyMemberState", gp.QueryPartyMemberState) + sub.apiFuncs.registApiFunction("LeaveParty", gp.LeaveParty) + sub.apiFuncs.registApiFunction("UpdatePartyMemberDocument", gp.UpdatePartyMemberDocument) + sub.apiFuncs.registApiFunction("UpdatePartyDocument", gp.UpdatePartyDocument) + sub.apiFuncs.registApiFunction("QueryPartyMembers", gp.QueryPartyMembers) + + return nil +} + +func (gp *groupParty) RegisterApiFunctions() { + +} + +// JoinParty : 그룹에 참가 +// - type : 그룹 타입 +// - 그룹 타입에 맞는 키(주로 _id) +// - member_id : 참가 멤버의 아이디 +// - body : 멤버의 속성 bson document +func (gp *groupParty) JoinParty(w http.ResponseWriter, r *http.Request) { + doc := bson.M{} + if err := readBsonDoc(r.Body, &doc); err != nil { + logger.Error("JoinParty failed. readBsonDoc returns err :", err) + w.WriteHeader(http.StatusBadRequest) + return + } + gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid") + if !ok { + logger.Println("JoinParty failed. gid is missing :", r.Form) + w.WriteHeader(http.StatusBadRequest) + return + } + mid, midok := gocommon.ReadObjectIDFormValue(r.Form, "mid") + if !midok { + 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 { + // 그룹이 없다. 실패 + w.Write([]byte("{}")) + return + } + + // 내 정보 업데이트할 때에도 사용됨 + if memdoc, err := gd.addMember(mid, doc); err == nil { + // 기존 유저에게 새 유저 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + gid.Hex(), + Body: map[string]any{ + gd.tid(mid): memdoc, + }, + Tag: []string{"MemberDocFull"}, + }) + + gp.sendEnterRoomMessage(gid, mid) + + // 새 멤버에 그룹 전체를 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "@" + mid.Hex(), + Body: gd.loadFull(), + Tag: []string{"GroupDocFull"}, + }) + writeBsonDoc(w, map[string]string{ + "gid": gid.Hex(), + "tid": gd.tid(mid), + }) + } else if err != nil { + logger.Error("JoinParty failed :", err) + w.WriteHeader(http.StatusInternalServerError) + } +} + +// InviteToParty : 초대 +// - type : 초대 타입 (required) +// - from : 초대하는 자 (required) +// - to : 초대받는 자 (required) +// - timeout : 초대 유지시간(optional. 없으면 config 기본 값) +// - (body) : 검색시 노출되는 document +func (gp *groupParty) InviteToParty(w http.ResponseWriter, r *http.Request) { + gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid") + if !ok { + logger.Println("InviteToParty failed. gid is missing :", r) + w.WriteHeader(http.StatusBadRequest) + return + } + mid, ok := gocommon.ReadObjectIDFormValue(r.Form, "mid") + if !ok { + logger.Println("InviteToParty failed. mid is missing :", r) + w.WriteHeader(http.StatusBadRequest) + return + } + + var reqdoc struct { + Inviter bson.M `bson:"inviter"` + Invitee bson.M `bson:"invitee"` + } + if err := readBsonDoc(r.Body, &reqdoc); err != nil { + logger.Println("InviteToParty failed. readBsonDoc returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + targetid, ok := reqdoc.Invitee["_mid"].(accountID) + if !ok { + logger.Println("InviteToParty failed. invitee mid is missing :", r) + w.WriteHeader(http.StatusBadRequest) + return + } + + // 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: reqdoc.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 { + gd, err = gp.createGroup(gid, mid, reqdoc.Inviter) + if err != nil { + logger.Println("InviteToParty failed. gp.createGroup() return err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + // 내가 wshandler room에 입장 + gp.sendEnterRoomMessage(gid, mid) + + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "@" + mid.Hex(), + Body: gd, + Tag: []string{"GroupDocFull"}, + }) + } + + newdoc, err := gd.addInvite(reqdoc.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: reqdoc.Inviter, + ExpireAtUTC: newdoc.InviteExpire, + }, + Tag: []string{"Invitation"}, + }) + + w.Write([]byte(gd.strid())) +} + +func (gp *groupParty) AcceptPartyInvitation(w http.ResponseWriter, r *http.Request) { + gid, _ := gocommon.ReadObjectIDFormValue(r.Form, "gid") + mid, _ := gocommon.ReadObjectIDFormValue(r.Form, "mid") + + var member bson.M + if err := readBsonDoc(r.Body, &member); err != nil { + logger.Error("AcceptPartyInvitation failed. readBsonDoc returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + cnt, err := gp.rh.Del(context.Background(), mid.Hex()).Result() + if err != nil { + logger.Error("AcceptPartyInvitation failed. gp.rh.Del returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if cnt == 0 { + // 만료됨 + w.Write([]byte("expired")) + return + } + + gd := &groupDoc{ + 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.sendEnterRoomMessage(gid, mid) + + // 새 멤버에 그룹 전체를 알림 + 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) DenyPartyInvitation(w http.ResponseWriter, r *http.Request) { + gid, _ := gocommon.ReadObjectIDFormValue(r.Form, "gid") + mid, _ := gocommon.ReadObjectIDFormValue(r.Form, "mid") + + gp.rh.Del(context.Background(), mid.Hex()).Result() + gd := groupDoc{ + id: gid, + rh: gp.rh, + } + gd.removeMember(mid) +} + +func (gp *groupParty) QueryPartyMemberState(w http.ResponseWriter, r *http.Request) { + mid, ok := gocommon.ReadStringFormValue(r.Form, "mid") + if !ok { + logger.Println("IsOnline failed. mid is missing :", r.Form) + w.WriteHeader(http.StatusBadRequest) + return + } + + state, _ := gp.rh.HGet(gp.rh.Context(), mid, "party_state").Result() + w.Write([]byte(state)) +} + +// LeaveParty : 그룹에서 나감 or 내보냄 +// - type : 그룹 타입 +// - 그룹 타입에 맞는 키(주로 _id) +// - member_id : 나갈 멤버의 아이디 +func (gp *groupParty) LeaveParty(w http.ResponseWriter, r *http.Request) { + gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid") + if !ok { + logger.Println("LeaveParty failed. gid is missing :", r.Form) + w.WriteHeader(http.StatusBadRequest) + return + } + mid, midok := gocommon.ReadObjectIDFormValue(r.Form, "mid") + if !midok { + logger.Println("LeaveParty failed. mid is missing") + w.WriteHeader(http.StatusBadRequest) + return + } + + gd := groupDoc{ + id: gid, + rh: gp.rh, + } + if err := gd.removeMember(mid); err != nil { + logger.Println("LeaveParty failed. gd.removeMember returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // 나한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "@" + mid.Hex(), + Body: bson.M{"gid": gid}, + Tag: []string{"GroupDocFull", gid.Hex()}, + }) + gp.sendLeaveRoomMessage(gid, mid) +} + +func (gp *groupParty) updateMemberDocument(gid groupID, mid accountID, doc bson.M) error { + gd := &groupDoc{ + id: gid, + rh: gp.rh, + } + prefixPath := fmt.Sprintf("$._members.%s.", gd.tid(mid)) + err := gp.rh.JSONMSetRel(gd.strid(), prefixPath, doc) + if err != nil { + return err + } + + if newstate, ok := doc["_state"]; ok { + gp.rh.HSet(gp.rh.Context(), mid.Hex(), "party_state", newstate).Result() + } + + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + gid.Hex(), + Body: map[string]any{ + gd.tid(mid): doc, + }, + Tag: []string{"MemberDocFragment"}, + }) + return nil +} + +func (gp *groupParty) UpdatePartyMemberDocument(w http.ResponseWriter, r *http.Request) { + mid, ok := gocommon.ReadObjectIDFormValue(r.Form, "mid") + if !ok { + logger.Println("UpdatePartyMemberDocument failed. member_id is missing") + w.WriteHeader(http.StatusBadRequest) + return + } + + gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid") + if !ok { + logger.Println("UpdatePartyMemberDocument failed. _id is missing") + w.WriteHeader(http.StatusBadRequest) + return + } + + var updatedoc bson.M + if err := readBsonDoc(r.Body, &updatedoc); err != nil { + logger.Error("UpdatePartyMemberDocument failed. body decoding error :", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + if err := gp.updateMemberDocument(gid, mid, updatedoc); err != nil { + logger.Println("UpdatePartyMemberDocument failed :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (gp *groupParty) updatePartyDocument(gid groupID, frag bson.M) error { + gd := groupDoc{ + 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) { + gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid") + if !ok { + logger.Println("UpdatePartyDocument failed. gid is missing") + w.WriteHeader(http.StatusBadRequest) + return + } + + var frag bson.M + if err := readBsonDoc(r.Body, &frag); err != nil { + logger.Error("UpdatePartyDocument failed. readBsonDoc err :", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + if err := gp.updatePartyDocument(gid, frag); err != nil { + logger.Error("UpdatePartyDocument failed. group.UpdatePartyDocument returns err :", err) + w.WriteHeader(http.StatusBadRequest) + return + } +} + +func (gp *groupParty) QueryPartyMembers(w http.ResponseWriter, r *http.Request) { + gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid") + if !ok { + logger.Println("QueryPartyMembers failed. gid is missing") + w.WriteHeader(http.StatusBadRequest) + return + } + + gd := groupDoc{ + id: gid, + rh: gp.rh, + } + + members, err := gd.getMembers() + if err != nil { + logger.Error("QueryPartyMembers failed. group.QueryPartyMembers returns err :", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + if err := writeBsonDoc(w, members); err != nil { + logger.Error("QueryPartyMembers failed. writeBsonDoc return err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (gp *groupParty) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*groupDoc, error) { + tid := makeTid(newid, charge) + + gd := &groupDoc{ + Members: map[string]any{ + tid: &memberDoc{ + Body: chargeDoc, + Invite: false, + InviteExpire: 0, + }, + }, + InCharge: tid, + + rh: gp.rh, + id: newid, + } + + _, err := gp.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX) + if err != nil { + return nil, err + } + return gd, nil +} + +func (gp *groupParty) find(id groupID) (*groupDoc, 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 &groupDoc{ + rh: gp.rh, + id: id, + }, nil +} + +func (gp *groupParty) memberDisconnected(room string, mid primitive.ObjectID) { + gid, err := primitive.ObjectIDFromHex(room) + if err != nil { + return + } + gd := &groupDoc{ + id: gid, + rh: gp.rh, + } + + tid := gd.tid(mid) + deleted, _ := gp.rh.JSONDel(room, "$._members."+tid) + if deleted > 0 { + // 퇴장을 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + room, + Body: bson.M{ + tid: bson.M{}, + }, + Tag: []string{"MemberDocFull"}, + }) + } +} + +func (gp *groupParty) ClientMessageReceved(sender *wshandler.Sender, mt wshandler.WebSocketMessageType, message any) { + if mt == wshandler.Connected { + gp.rh.HSet(gp.rh.Context(), sender.Accid.Hex(), "party_state", "connected").Result() + } else if mt == wshandler.Disconnected { + rooms := message.([]string) + for _, roomname := range rooms { + gp.memberDisconnected(roomname, sender.Accid) + } + gp.rh.HDel(gp.rh.Context(), sender.Accid.Hex(), "party_state").Result() + } else if mt == wshandler.BinaryMessage { + commandline := message.([]any) + cmd := commandline[0].(string) + args := commandline[1:] + switch cmd { + case "UpdatePartyMemberDocument": + gidobj, _ := primitive.ObjectIDFromHex(args[0].(string)) + doc := args[1].(map[string]any) + gp.updateMemberDocument(gidobj, sender.Accid, doc) + + case "UpdatePartyDocument": + gidobj, _ := primitive.ObjectIDFromHex(args[0].(string)) + doc := args[1].(map[string]any) + gp.updatePartyDocument(gidobj, doc) + } + } +} diff --git a/core/tavern.go b/core/tavern.go index 18b8b1a..556fa70 100644 --- a/core/tavern.go +++ b/core/tavern.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net" "net/http" @@ -18,19 +19,12 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/bsonrw" - "go.mongodb.org/mongo-driver/bson/primitive" ) const ( defaultMaxMemory = 32 << 10 // 32 KB ) -func writeBsonArr(w io.Writer, src []bson.M) error { - return writeBsonDoc(w, bson.M{ - "r": src, - }) -} - func writeBsonDoc[T any](w io.Writer, src T) error { rw, err := bsonrw.NewBSONValueWriter(w) if err != nil { @@ -71,9 +65,9 @@ func readBsonDoc(r io.Reader, src any) error { type TavernConfig struct { gocommon.RegionStorageConfig `json:",inline"` - GroupTypes map[string]*groupConfig `json:"tavern_group_types"` - MaingateApiToken string `json:"maingate_api_token"` - RedisURL string `json:"tavern_redis_url"` + Group2 map[string]configDocument `json:"tavern_group_types"` + MaingateApiToken string `json:"maingate_api_token"` + RedisURL string `json:"tavern_redis_url"` macAddr string } @@ -90,7 +84,7 @@ type subTavern struct { wsh *wshandler.WebsocketHandler region string groups map[string]group - methods map[string]reflect.Method + apiFuncs *apiFuncsContainer } func getMacAddr() (string, error) { @@ -147,15 +141,6 @@ func (tv *Tavern) prepare(ctx context.Context) error { for region, addr := range config.RegionStorage { var dbconn gocommon.MongoClient var err error - var groupinstance group - - var tmp *subTavern - methods := make(map[string]reflect.Method) - tp := reflect.TypeOf(tmp) - for i := 0; i < tp.NumMethod(); i++ { - method := tp.Method(i) - methods[method.Name] = method - } redisClient, err := gocommon.NewRedisClient(addr.Redis["tavern"]) if err != nil { @@ -167,30 +152,32 @@ func (tv *Tavern) prepare(ctx context.Context) error { mongoClient: dbconn, redisClient: redisClient, region: region, - methods: methods, + apiFuncs: &apiFuncsContainer{ + normfuncs: make(map[string]apiFuncType), + funcs: make(map[string][]apiFuncType), + }, } groups := make(map[string]group) - for typename, cfg := range config.GroupTypes { - cfg.Name = typename - if cfg.Transient { - groupinstance, err = cfg.prepareInMemory(ctx, typename, sub) - //} else { - // TODO : db - // if !dbconn.Connected() { - // dbconn, err = gocommon.NewMongoClient(ctx, url.Mongo, region) - // if err != nil { - // return err - // } - // } - // groupinstance, err = cfg.preparePersistent(ctx, region, dbconn, tv.wsh) + for typename, cfg := range config.Group2 { + gtype, ok := groupTypeContainer()[typename] + if !ok { + return fmt.Errorf("%s group type is not valid", typename) } - if err != nil { + + if !gtype.Implements(reflect.TypeOf((*group)(nil)).Elem()) { + return fmt.Errorf("%s is not implement proper interface", typename) + } + ptrvalue := reflect.New(gtype.Elem()) + instance := ptrvalue.Interface().(group) + if err := instance.Initialize(sub, cfg); err != nil { return err } - groups[typename] = groupinstance + groups[typename] = instance } + sub.groups = groups + sub.apiFuncs.normalize() tv.subTaverns = append(tv.subTaverns, sub) } @@ -216,44 +203,37 @@ func (tv *Tavern) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux, func (sub *subTavern) OnClientMessageReceived(sender *wshandler.Sender, messageType wshandler.WebSocketMessageType, body io.Reader) { if messageType == wshandler.Connected { logger.Println("OnClientMessageReceived : connected ", sender.Accid.Hex()) + sub.redisClient.HSet(sub.redisClient.Context(), sender.Accid.Hex(), "_ts", time.Now().UTC().Unix()).Result() + + for _, gt := range sub.groups { + gt.ClientMessageReceved(sender, messageType, nil) + } } else if messageType == wshandler.Disconnected { var rooms []string dec := json.NewDecoder(body) if err := dec.Decode(&rooms); err == nil { - for _, roomname := range rooms { - for _, gt := range sub.groups { - gt.MemberDisconnected(roomname, sender.Accid) - } + for _, gt := range sub.groups { + gt.ClientMessageReceved(sender, messageType, rooms) } } + sub.redisClient.Del(sub.redisClient.Context(), sender.Accid.Hex()).Result() logger.Println("OnClientMessageReceived : disconnected ", sender.Accid.Hex()) } else if messageType == wshandler.BinaryMessage { - var msg map[string][]any + var commandline []any dec := json.NewDecoder(body) - if err := dec.Decode(&msg); err == nil { - for cmd, args := range msg { - switch cmd { - case "EnterChannel": - sub.wsh.EnterRoom(sub.region, args[0].(string), sender.Accid) + if err := dec.Decode(&commandline); err == nil { + cmd := commandline[0].(string) + args := commandline[1:] + switch cmd { + case "EnterChannel": + sub.wsh.EnterRoom(sub.region, args[0].(string), sender.Accid) - case "LeaveChannel": - sub.wsh.LeaveRoom(sub.region, args[0].(string), sender.Accid) + case "LeaveChannel": + sub.wsh.LeaveRoom(sub.region, args[0].(string), sender.Accid) - case "UpdateGroupMemberDocument": - typename := args[0].(string) - gidobj, _ := primitive.ObjectIDFromHex(args[1].(string)) - doc := args[2].(map[string]any) - if group := sub.groups[typename]; group != nil { - group.UpdateMemberDocument(gidobj, sender.Accid, doc) - } - - case "UpdateGroupDocument": - typename := args[0].(string) - gidobj, _ := primitive.ObjectIDFromHex(args[1].(string)) - doc := args[2].(map[string]any) - if group := sub.groups[typename]; group != nil { - group.UpdateGroupDocument(gidobj, doc) - } + default: + for _, gt := range sub.groups { + gt.ClientMessageReceved(sender, messageType, commandline) } } } @@ -292,6 +272,9 @@ func (sub *subTavern) api(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) return } + if r.PostForm == nil { + r.ParseMultipartForm(defaultMaxMemory) + } operation := r.URL.Query().Get("operation") if len(operation) == 0 { @@ -299,23 +282,5 @@ func (sub *subTavern) api(w http.ResponseWriter, r *http.Request) { return } - method, ok := sub.methods[operation] - if !ok { - // 없는 operation - logger.Println("fail to call api. operation is not valid :", operation) - w.WriteHeader(http.StatusBadRequest) - return - } - - if r.PostForm == nil { - r.ParseMultipartForm(defaultMaxMemory) - } - - args := []reflect.Value{ - reflect.ValueOf(sub), - reflect.ValueOf(w), - reflect.ValueOf(r), - } - - method.Func.Call(args) + sub.apiFuncs.call(operation, w, r) }