party로 그룹 변경

This commit is contained in:
2023-07-25 18:11:02 +09:00
parent bb6a741d63
commit 310397dd2b
7 changed files with 881 additions and 1262 deletions

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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)
}

22
core/group_chat.go Normal file
View File

@ -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) {
}

View File

@ -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
}

759
core/group_party.go Normal file
View File

@ -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)
}
}
}

View File

@ -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,7 +65,7 @@ func readBsonDoc(r io.Reader, src any) error {
type TavernConfig struct {
gocommon.RegionStorageConfig `json:",inline"`
GroupTypes map[string]*groupConfig `json:"tavern_group_types"`
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,22 +203,27 @@ 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)
}
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 {
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)
@ -239,21 +231,9 @@ func (sub *subTavern) OnClientMessageReceived(sender *wshandler.Sender, messageT
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)
}