1222 lines
28 KiB
Go
1222 lines
28 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/gob"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"repositories.action2quare.com/ayo/gocommon/logger"
|
|
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
|
"repositories.action2quare.com/ayo/tavern/core/flag"
|
|
"repositories.action2quare.com/ayo/tavern/core/rpc"
|
|
|
|
"github.com/go-redis/redis/v8"
|
|
"github.com/gorilla/websocket"
|
|
"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
|
|
|
|
var everyHost, _ = primitive.ObjectIDFromHex("010203040506070809101112")
|
|
|
|
type Invitation struct {
|
|
GroupID groupID `json:"gid"`
|
|
TicketID ticketID `json:"tid"`
|
|
Inviter bson.M `json:"inviter"`
|
|
ExpireAtUTC int64 `json:"expire_at_utc"`
|
|
}
|
|
|
|
type memberDocCommon struct {
|
|
Body bson.M
|
|
Invite bool
|
|
InviteExpire time.Time
|
|
JoinTime int64
|
|
}
|
|
|
|
// 플레이어한테 공유하는 멤버 정보
|
|
type PublicMemberDoc struct {
|
|
memberDocCommon `json:",inline"`
|
|
Tid ticketID
|
|
}
|
|
|
|
type FullGroupDoc struct {
|
|
Gid groupID
|
|
DM string
|
|
AllMembers []*PublicMemberDoc `json:",omitempty"`
|
|
Body GroupDocBody `json:",omitempty"`
|
|
}
|
|
|
|
type GroupDocBody bson.M
|
|
type InvitationFail bson.M
|
|
|
|
type memberDoc struct {
|
|
memberDocCommon `json:",inline"`
|
|
|
|
// underscore keys in Hidden
|
|
Hidden bson.M
|
|
rconn *wshandler.Richconn
|
|
Mid accountID
|
|
}
|
|
|
|
type groupDoc struct {
|
|
sync.Mutex
|
|
|
|
Body GroupDocBody
|
|
|
|
InCharge accountID
|
|
tickets map[ticketID]*memberDoc
|
|
createTime time.Time
|
|
}
|
|
|
|
func init() {
|
|
gob.Register(PublicMemberDoc{})
|
|
}
|
|
|
|
func (gd *groupDoc) updateBodyWithBson(src []byte) ([]byte, error) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
err := bson.Unmarshal(src, &gd.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return bson.Marshal(gd.Body)
|
|
}
|
|
|
|
func (gd *groupDoc) updateBodyWithJson(src []byte) ([]byte, error) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
err := json.Unmarshal(src, &gd.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return json.Marshal(makeTypeMessage(gd.Body))
|
|
}
|
|
|
|
func (gd *groupDoc) updateBodyBsonToJson(bsonSrc []byte) (jsonBt []byte, err error) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
err = bson.Unmarshal(bsonSrc, &gd.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return json.Marshal(makeTypeMessage(gd.Body))
|
|
}
|
|
|
|
func (gd *groupDoc) updateBody(bsonSrc []byte) error {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
return bson.Unmarshal(bsonSrc, &gd.Body)
|
|
}
|
|
|
|
func (gd *groupDoc) addInvite(inviteeDoc bson.M, rconn *wshandler.Richconn, ttl time.Duration, max int) (ticketID, *memberDoc) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
mid := inviteeDoc["_mid"].(accountID)
|
|
body := inviteeDoc["body"].(bson.M)
|
|
|
|
// 초대 가능한 빈 자리가 있나
|
|
now := time.Now().UTC()
|
|
if len(gd.tickets) < max {
|
|
tid := primitive.NewObjectID()
|
|
newdoc := &memberDoc{
|
|
memberDocCommon: memberDocCommon{
|
|
Body: body,
|
|
Invite: true,
|
|
InviteExpire: now.Add(ttl),
|
|
},
|
|
rconn: rconn,
|
|
Mid: mid,
|
|
}
|
|
gd.tickets[tid] = newdoc
|
|
return tid, newdoc
|
|
}
|
|
|
|
for oldtid, mem := range gd.tickets {
|
|
if !mem.Invite {
|
|
continue
|
|
}
|
|
if mem.InviteExpire.Before(now) {
|
|
delete(gd.tickets, oldtid)
|
|
tid := primitive.NewObjectID()
|
|
newdoc := &memberDoc{
|
|
memberDocCommon: memberDocCommon{
|
|
Body: body,
|
|
Invite: true,
|
|
InviteExpire: now.Add(ttl),
|
|
},
|
|
rconn: rconn,
|
|
Mid: mid,
|
|
}
|
|
gd.tickets[tid] = newdoc
|
|
return tid, newdoc
|
|
}
|
|
}
|
|
|
|
return primitive.NilObjectID, nil
|
|
}
|
|
|
|
func seperateHidden(in bson.M) (public bson.M, hidden bson.M) {
|
|
for k, v := range in {
|
|
if k[0] == '_' {
|
|
if hidden == nil {
|
|
hidden = make(bson.M)
|
|
}
|
|
hidden[k] = v
|
|
}
|
|
}
|
|
|
|
for k := range hidden {
|
|
delete(in, k)
|
|
}
|
|
return in, hidden
|
|
}
|
|
|
|
func (gd *groupDoc) addInCharge(mid accountID, rconn *wshandler.Richconn, doc bson.M) (ticketID, *memberDoc) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
if !gd.InCharge.IsZero() {
|
|
return primitive.NilObjectID, nil
|
|
}
|
|
|
|
gd.InCharge = mid
|
|
newtid := primitive.NewObjectID()
|
|
doc, hidden := seperateHidden(doc)
|
|
newdoc := &memberDoc{
|
|
memberDocCommon: memberDocCommon{
|
|
Body: doc,
|
|
Invite: false,
|
|
JoinTime: time.Now().UTC().Unix(),
|
|
},
|
|
rconn: rconn,
|
|
Mid: mid,
|
|
Hidden: hidden,
|
|
}
|
|
|
|
gd.tickets[newtid] = newdoc
|
|
if gd.Body == nil {
|
|
gd.Body = GroupDocBody(make(bson.M))
|
|
}
|
|
gd.Body["incharge"] = newtid.Hex()
|
|
return newtid, newdoc
|
|
}
|
|
|
|
func (gd *groupDoc) addMember(mid accountID, tid *ticketID, doc bson.M) (*memberDoc, bool) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
var memdoc *memberDoc
|
|
isNew := false
|
|
if tid.IsZero() {
|
|
for oldtid, d := range gd.tickets {
|
|
if d.Mid == mid {
|
|
memdoc = d
|
|
*tid = oldtid
|
|
isNew = true
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
var ok bool
|
|
memdoc, ok = gd.tickets[*tid]
|
|
if !ok {
|
|
// 티켓이 업네?
|
|
return nil, false
|
|
}
|
|
|
|
if memdoc.Mid != mid {
|
|
// 내 티켓이 아니네?
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
doc, hidden := seperateHidden(doc)
|
|
if memdoc != nil {
|
|
memdoc.Body = doc
|
|
memdoc.Hidden = hidden
|
|
|
|
if memdoc.Invite {
|
|
isNew = true
|
|
memdoc.Invite = false
|
|
}
|
|
|
|
if memdoc.JoinTime == 0 {
|
|
memdoc.JoinTime = time.Now().UTC().Unix()
|
|
}
|
|
}
|
|
return memdoc, isNew
|
|
}
|
|
|
|
func (gd *groupDoc) removeMember(mid accountID, tid *ticketID) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
if tid.IsZero() {
|
|
for t, mem := range gd.tickets {
|
|
if mem.Mid == mid {
|
|
*tid = t
|
|
delete(gd.tickets, t)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
delete(gd.tickets, *tid)
|
|
|
|
if gd.InCharge == mid {
|
|
gd.InCharge = primitive.NilObjectID
|
|
}
|
|
}
|
|
|
|
func (gd *groupDoc) conns(includeInvitee bool) (out []*wshandler.Richconn) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
for _, mem := range gd.tickets {
|
|
if mem.rconn != nil {
|
|
if !includeInvitee && mem.Invite {
|
|
continue
|
|
}
|
|
out = append(out, mem.rconn)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (gd *groupDoc) ticket(mid accountID) ticketID {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
if mid.IsZero() {
|
|
return primitive.NilObjectID
|
|
}
|
|
|
|
for t, m := range gd.tickets {
|
|
if m.Mid == mid {
|
|
return t
|
|
}
|
|
}
|
|
return primitive.NilObjectID
|
|
}
|
|
|
|
func (gd *groupDoc) member(tid ticketID) *memberDoc {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
return gd.tickets[tid]
|
|
}
|
|
|
|
func (gd *groupDoc) memberByAccount(mid accountID) (ticketID, *memberDoc) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
for tid, doc := range gd.tickets {
|
|
if doc.Mid == mid {
|
|
return tid, doc
|
|
}
|
|
}
|
|
return primitive.NilObjectID, nil
|
|
}
|
|
|
|
func (gd *groupDoc) modifyMemberDocument(mid accountID, tid *ticketID, cb func(b *memberDoc)) *memberDoc {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
if tid.IsZero() {
|
|
for t, mem := range gd.tickets {
|
|
if mem.Mid == mid {
|
|
*tid = t
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if tid.IsZero() {
|
|
return nil
|
|
}
|
|
|
|
if mem := gd.tickets[*tid]; mem != nil {
|
|
cb(mem)
|
|
return mem
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (gd *groupDoc) overwriteMemberDocument(mid accountID, tid *ticketID, raw []byte) *memberDoc {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
if tid.IsZero() {
|
|
for t, mem := range gd.tickets {
|
|
if mem.Mid == mid {
|
|
*tid = t
|
|
json.Unmarshal(raw, &mem.Body)
|
|
return mem
|
|
}
|
|
}
|
|
}
|
|
|
|
if mem := gd.tickets[*tid]; mem != nil {
|
|
var newbody primitive.M
|
|
json.Unmarshal(raw, &newbody)
|
|
mem.Body = newbody
|
|
return mem
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (gd *groupDoc) iterateMembers(cb func(ticketID, *memberDoc)) {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
for k, v := range gd.tickets {
|
|
cb(k, v)
|
|
}
|
|
}
|
|
|
|
func (gd *groupDoc) serializeFull(gid groupID, directMessageChanName string) []byte {
|
|
gd.Lock()
|
|
defer gd.Unlock()
|
|
|
|
var output []*PublicMemberDoc
|
|
for k, v := range gd.tickets {
|
|
if v.Invite {
|
|
// 아직 초대 중인 대상. 패스
|
|
continue
|
|
}
|
|
|
|
output = append(output, &PublicMemberDoc{
|
|
memberDocCommon: v.memberDocCommon,
|
|
Tid: k,
|
|
})
|
|
}
|
|
|
|
bt, _ := json.Marshal(makeTypeMessage(FullGroupDoc{
|
|
Gid: gid,
|
|
DM: directMessageChanName,
|
|
AllMembers: output,
|
|
Body: gd.Body,
|
|
}))
|
|
|
|
return bt
|
|
}
|
|
|
|
type groupContainer struct {
|
|
sync.Mutex
|
|
groupDocs map[groupID]*groupDoc
|
|
}
|
|
|
|
type groupInMemory struct {
|
|
*groupConfig
|
|
groupDocSync func(groupID, []byte) error
|
|
memberSync func(groupID, accountID, ticketID, *memberDoc, bool) error
|
|
rpcCall func([]byte) error
|
|
hasConn func(accountID) *wshandler.Richconn
|
|
groups groupContainer
|
|
}
|
|
|
|
func (gc *groupContainer) add(id groupID, doc *groupDoc) {
|
|
gc.Lock()
|
|
defer gc.Unlock()
|
|
|
|
gc.groupDocs[id] = doc
|
|
}
|
|
|
|
func (gc *groupContainer) createWithID(newid groupID, base bson.M) (groupID, *groupDoc) {
|
|
gc.Lock()
|
|
defer gc.Unlock()
|
|
|
|
if _, ok := gc.groupDocs[newid]; ok {
|
|
return primitive.NilObjectID, nil
|
|
}
|
|
|
|
newdoc := newGroupDoc(base)
|
|
gc.groupDocs[newid] = newdoc
|
|
|
|
return newid, newdoc
|
|
}
|
|
|
|
func (gc *groupContainer) delete(gid groupID) {
|
|
gc.Lock()
|
|
defer gc.Unlock()
|
|
|
|
delete(gc.groupDocs, gid)
|
|
}
|
|
|
|
func (gc *groupContainer) find(id groupID) *groupDoc {
|
|
gc.Lock()
|
|
defer gc.Unlock()
|
|
|
|
if found, ok := gc.groupDocs[id]; ok {
|
|
return found
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newGroupDoc(base bson.M) *groupDoc {
|
|
return &groupDoc{
|
|
Body: GroupDocBody(base),
|
|
createTime: time.Now().UTC(),
|
|
tickets: make(map[ticketID]*memberDoc),
|
|
}
|
|
}
|
|
|
|
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")
|
|
var errFuncNameIsMissing = errors.New("how func name is missin")
|
|
|
|
func (gm *groupInMemory) callProxyRpc(target accountID, name string, args ...any) error {
|
|
bt, err := rpc.Encode(target, name, args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return gm.rpcCall(bt)
|
|
}
|
|
|
|
type rpcTarget struct {
|
|
gm *groupInMemory
|
|
target accountID
|
|
}
|
|
|
|
func (rt rpcTarget) call(args ...any) error {
|
|
pc := make([]uintptr, 1)
|
|
n := runtime.Callers(2, pc[:])
|
|
if n < 1 {
|
|
return nil
|
|
}
|
|
|
|
frame, _ := runtime.CallersFrames(pc).Next()
|
|
funcname := path.Ext(frame.Func.Name())
|
|
if len(funcname) > 0 {
|
|
funcname = funcname[1:]
|
|
return rt.gm.callProxyRpc(rt.target, funcname, args...)
|
|
}
|
|
|
|
return errFuncNameIsMissing
|
|
}
|
|
|
|
func (gm *groupInMemory) rpc(target accountID) rpcTarget {
|
|
return rpcTarget{
|
|
gm: gm,
|
|
target: target,
|
|
}
|
|
}
|
|
|
|
var errNoEmptySlot = errors.New("no more seat in group")
|
|
|
|
func (gm *groupInMemory) Join(gid groupID, mid accountID, tid ticketID, doc bson.M) (ticketID, error) {
|
|
group := gm.groups.find(gid)
|
|
if group == nil {
|
|
// 그룹이 없다. 실패
|
|
return primitive.NilObjectID, errGroupNotExist
|
|
}
|
|
|
|
// 내 정보 업데이트할 때에도 사용됨
|
|
// 굳이 InCharge가 있는 호스트가 아니어도 가능
|
|
if memdoc, isNew := group.addMember(mid, &tid, doc); memdoc != nil {
|
|
gm.memberSync(gid, mid, tid, memdoc, isNew)
|
|
}
|
|
|
|
return tid, nil
|
|
}
|
|
|
|
func (gm *groupInMemory) FindTicketID(gid groupID, mid groupID) ticketID {
|
|
return primitive.NilObjectID
|
|
}
|
|
|
|
func makeTypeMessage[T any](msg T) bson.M {
|
|
var ptr *T
|
|
name := reflect.TypeOf(ptr).Elem().Name()
|
|
return bson.M{name: msg}
|
|
}
|
|
|
|
func sendTypedMessageDirect[T any](rconn *wshandler.Richconn, msg T) {
|
|
bt, _ := json.Marshal(makeTypeMessage(msg))
|
|
rconn.WriteBytes(bt)
|
|
}
|
|
|
|
func sendTypedMessage[T any](gm *groupInMemory, target accountID, msg T) {
|
|
bt, _ := json.Marshal(makeTypeMessage(msg))
|
|
gm.SendMessage(target, bt)
|
|
}
|
|
|
|
func (gm *groupInMemory) SendMessage(target accountID, msg []byte) {
|
|
rconn := gm.hasConn(target)
|
|
if rconn != nil {
|
|
rconn.WriteBytes(msg)
|
|
} else {
|
|
gm.rpc(target).call(target, msg)
|
|
}
|
|
}
|
|
|
|
func multicast(conns []*wshandler.Richconn, raw []byte) {
|
|
for _, rconn := range conns {
|
|
rconn.WriteBytes(raw)
|
|
}
|
|
}
|
|
func multicastTyped[T any](conns []*wshandler.Richconn, msg T) {
|
|
bt, _ := json.Marshal(makeTypeMessage(msg))
|
|
go multicast(conns, bt)
|
|
}
|
|
|
|
func broadcastTypedMessage[T any](gm *groupInMemory, gid groupID, msg T) {
|
|
if gd := gm.groups.find(gid); gd != nil {
|
|
bt, _ := json.Marshal(makeTypeMessage(msg))
|
|
go multicast(gd.conns(false), bt)
|
|
}
|
|
}
|
|
|
|
var errInviteeDocMidMissing = errors.New("inviteeDoc must have '_mid' field")
|
|
|
|
func (gm *groupInMemory) SendInvitationFailed(mid accountID, inviteeDoc bson.M) error {
|
|
delete(inviteeDoc, "_mid")
|
|
rconn := gm.hasConn(mid)
|
|
if rconn == nil {
|
|
return gm.rpc(mid).call(mid, inviteeDoc)
|
|
}
|
|
|
|
sendTypedMessage(gm, mid, InvitationFail(inviteeDoc))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (gm *groupInMemory) InviteImplement(gid groupID, mid accountID, inviteeDoc bson.M, inviterDoc bson.M) error {
|
|
targetid := inviteeDoc["_mid"].(accountID)
|
|
|
|
// invitee에게 알림
|
|
// invitee의 rconn이 종료될 때 그룹에 반영해야 하므로 rconn을 찾자
|
|
rconn := gm.hasConn(targetid)
|
|
if rconn == nil {
|
|
return gm.rpc(targetid).call(gid, inviteeDoc, inviterDoc)
|
|
}
|
|
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
return errGroupNotExist
|
|
}
|
|
|
|
if rconn.HasOnCloseFunc("member_remove_invite") {
|
|
// 이미 초대 중이다.
|
|
// inviter한테 알려줘야 한다.
|
|
return gm.SendInvitationFailed(mid, inviteeDoc)
|
|
}
|
|
|
|
tid, newdoc := gd.addInvite(inviteeDoc, rconn, time.Duration(gm.InviteExpire)*time.Second, gm.MaxMember)
|
|
if newdoc == nil {
|
|
return errNoEmptySlot
|
|
}
|
|
|
|
rconn.RegistOnCloseFunc("member_remove_invite", func() {
|
|
gd.removeMember(targetid, &tid)
|
|
gm.memberSync(gid, targetid, tid, nil, false)
|
|
})
|
|
|
|
gm.memberSync(gid, targetid, tid, newdoc, false)
|
|
sendTypedMessage(gm, targetid, Invitation{
|
|
GroupID: gid,
|
|
TicketID: tid,
|
|
Inviter: inviterDoc,
|
|
ExpireAtUTC: newdoc.InviteExpire.Unix(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
var errAlreayMember = errors.New("this target is already member")
|
|
|
|
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
|
|
}
|
|
|
|
if !gid.IsZero() {
|
|
if gd := gm.groups.find(gid); gd != nil {
|
|
if gd.InCharge != mid {
|
|
// 이러면 안된다.
|
|
// 초대는 InCharge만 할 수 있음
|
|
return "", nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// gid는 미리 만들어 놔야함.
|
|
// 초대하는 클라이언트가 아직 group을 소유하지 않고 있을 수 있다.
|
|
// mid의 rconn이 이 호스트에 없더라도 gid는 이 request를 보낸 클라이언트가 받아야 하기 떄문
|
|
if gid.IsZero() {
|
|
gid = primitive.NewObjectID()
|
|
}
|
|
|
|
rconn := gm.hasConn(mid)
|
|
if rconn == nil {
|
|
// mid가 있는 곳에서 처리를 해야 접속 끊겼을 때 콜백을 먼저 등록할 수 있다.
|
|
// 콜백이 rconn에 먼저 등록되지 않으면 좀비 group이 생길 가능성이 생긴다.
|
|
return gid.Hex(), gm.rpc(mid).call(gid, mid, inviteeDoc, inviteeDoc)
|
|
}
|
|
|
|
// 이제 여기는 mid가 InCharge이면서 rconn이 존재
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
_, gd = gm.groups.createWithID(gid, bson.M{})
|
|
tid, newdoc := gd.addInCharge(mid, rconn, inviterDoc)
|
|
rconn.RegistOnCloseFunc("member_remove", func() {
|
|
// 내가 InCharge이므로 접속이 종료될 때 그룹을 해체한다.
|
|
gm.groupDocSync(gid, nil)
|
|
})
|
|
bt, err := bson.Marshal(gd.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
gm.groupDocSync(gid, bt)
|
|
gm.memberSync(gid, mid, tid, newdoc, true)
|
|
} else {
|
|
// targetid가 이미 멤버인지 미리 확인 가능
|
|
if !gd.ticket(targetid).IsZero() {
|
|
// 이미 멤버네
|
|
return "", errAlreayMember
|
|
}
|
|
}
|
|
|
|
return gid.Hex(), gm.InviteImplement(gid, mid, inviteeDoc, inviterDoc)
|
|
}
|
|
|
|
func (gm *groupInMemory) UpdateGroupMember(gid groupID, mid accountID, tid ticketID, doc bson.M) error {
|
|
return nil
|
|
}
|
|
func (gm *groupInMemory) CancelInvitation(gid groupID, tid ticketID) error {
|
|
return nil
|
|
}
|
|
|
|
func (gm *groupInMemory) AcceptInvitation(gid groupID, mid accountID, tid ticketID, member bson.M) (groupID, error) {
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
return primitive.NilObjectID, errGroupNotExist
|
|
}
|
|
|
|
rconn := gm.hasConn(mid)
|
|
if rconn == nil {
|
|
return gid, gm.rpc(mid).call(gid, mid, tid, member)
|
|
}
|
|
|
|
oldFunc := rconn.UnregistOnCloseFunc("member_remove")
|
|
if oldFunc != nil {
|
|
// 기존 멤버였으면 탈퇴 처리
|
|
oldFunc()
|
|
}
|
|
|
|
inviteFunc := rconn.UnregistOnCloseFunc("member_remove_invite")
|
|
rconn.RegistOnCloseFunc("member_remove", inviteFunc)
|
|
|
|
result, isNew := gd.addMember(mid, &tid, member)
|
|
if result != nil {
|
|
return gid, gm.memberSync(gid, mid, tid, result, isNew)
|
|
}
|
|
|
|
// 실패
|
|
return primitive.NilObjectID, nil
|
|
}
|
|
|
|
func (gm *groupInMemory) DenyInvitation(gid groupID, mid accountID, tid ticketID) error {
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
return errGroupNotExist
|
|
}
|
|
|
|
rconn := gm.hasConn(mid)
|
|
if rconn == nil {
|
|
return gm.rpc(mid).call(gid, mid, tid)
|
|
}
|
|
|
|
inviteFunc := rconn.UnregistOnCloseFunc("member_remove_invite")
|
|
if inviteFunc != nil {
|
|
inviteFunc() // removeMember는 여기에 들어있다.
|
|
return nil
|
|
}
|
|
|
|
gd.removeMember(mid, &tid)
|
|
return gm.memberSync(gid, mid, tid, nil, false)
|
|
}
|
|
|
|
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) DropPausedMember(gid primitive.ObjectID, mid primitive.ObjectID) error {
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
return errGroupNotExist
|
|
}
|
|
|
|
tid, memdoc := gd.memberByAccount(mid)
|
|
if memdoc == nil {
|
|
return errNotMember
|
|
}
|
|
|
|
if _, ok := memdoc.Body["paused"]; ok {
|
|
// 드랍해야 한다.
|
|
if gd.InCharge == mid {
|
|
// 내가 방장인 경우
|
|
gm.groupDocSync(gid, nil)
|
|
} else {
|
|
// 내가 방장이 아닌 경우
|
|
gd.removeMember(mid, &tid)
|
|
gm.memberSync(gid, mid, tid, nil, false)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (gm *groupInMemory) PauseMember(gid primitive.ObjectID, mid primitive.ObjectID, rconn *wshandler.Richconn) error {
|
|
rconn.UnregistOnCloseFunc("member_remove")
|
|
rconn.UnregistOnCloseFunc("member_remove_invite")
|
|
rconn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "pause"), time.Time{})
|
|
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
return errGroupNotExist
|
|
}
|
|
|
|
tid := primitive.NilObjectID
|
|
newdoc := gd.modifyMemberDocument(mid, &tid, func(memdoc *memberDoc) {
|
|
memdoc.Body["paused"] = true
|
|
memdoc.rconn = nil
|
|
})
|
|
|
|
return gm.memberSync(gid, mid, tid, newdoc, false)
|
|
}
|
|
|
|
func (gm *groupInMemory) QueryMembers(gid groupID, reqID accountID, projection string, after primitive.Timestamp) (map[string]bson.M, error) {
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
return nil, errGroupNotExist
|
|
}
|
|
|
|
if gd.InCharge != reqID {
|
|
return nil, errGroupNotExist
|
|
}
|
|
|
|
outdocs := make(map[string]bson.M)
|
|
if len(projection) > 0 {
|
|
projkeys := map[string]bool{}
|
|
for _, p := range strings.Split(projection, ",") {
|
|
if p[0] == '+' {
|
|
projkeys[strings.TrimSpace(p[1:])] = true
|
|
} else {
|
|
projkeys[strings.TrimSpace(p)] = true
|
|
}
|
|
}
|
|
|
|
gd.iterateMembers(func(tid ticketID, memdoc *memberDoc) {
|
|
outdoc := bson.M{}
|
|
for k := range projkeys {
|
|
if k[0] == '_' {
|
|
outdoc[k] = memdoc.Hidden[k]
|
|
} else {
|
|
outdoc[k] = memdoc.Body[k]
|
|
}
|
|
}
|
|
outdocs[memdoc.Mid.Hex()] = outdoc
|
|
})
|
|
} else {
|
|
gd.iterateMembers(func(tid ticketID, memdoc *memberDoc) {
|
|
outdoc := bson.M{}
|
|
for k, v := range memdoc.Hidden {
|
|
outdoc[k] = v
|
|
}
|
|
for k, v := range memdoc.Body {
|
|
outdoc[k] = v
|
|
}
|
|
outdocs[memdoc.Mid.Hex()] = outdoc
|
|
})
|
|
}
|
|
|
|
return outdocs, nil
|
|
}
|
|
|
|
func (gm *groupInMemory) QueryMember(gid groupID, mid accountID, tid ticketID, projection string) (bson.M, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
var errHaveNoAuthority = errors.New("cannot kick other member")
|
|
var errNotMember = errors.New("ticket is not in this group")
|
|
|
|
func (gm *groupInMemory) Leave(gid groupID, mid accountID, tid ticketID) error {
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
return errGroupNotExist
|
|
}
|
|
|
|
// mid가 InCharge인 경우에는 tid가 누구든 내쫓고,
|
|
// mid가 InCharge가 아닌 경우는 tid가 mid일 경우에만 나갈 수 있다.
|
|
memdoc := gd.member(tid)
|
|
if memdoc == nil {
|
|
return errNotMember
|
|
}
|
|
targetmid := memdoc.Mid
|
|
|
|
// 내가 방장이면 아무나 내보낼 수 있다.
|
|
if gd.InCharge != mid && targetmid != mid {
|
|
// targetmid와 mid가 같아야 한다. 방장이 아니므로 나는 나만 내보낼 수 있다.
|
|
return errHaveNoAuthority
|
|
}
|
|
|
|
// targetmid의 "member_remove" 함수를 등록 해제해야 하므로 rconn이 있는 곳에서 하자
|
|
rconn := gm.hasConn(targetmid)
|
|
if rconn == nil {
|
|
return gm.rpc(targetmid).call(gid, mid, tid)
|
|
}
|
|
|
|
if oldfunc := rconn.UnregistOnCloseFunc("member_remove"); oldfunc != nil {
|
|
oldfunc() // 이 안에 다 있다.
|
|
}
|
|
|
|
// 나한테는 빈 FullGroupDoc을 보낸다.
|
|
sendTypedMessageDirect(rconn, FullGroupDoc{
|
|
Gid: gid,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (gm *groupInMemory) UpdateMemberDocument(gid groupID, mid accountID, doc bson.M) error {
|
|
return nil
|
|
}
|
|
func (gm *groupInMemory) Dismiss(gid groupID) error {
|
|
return nil
|
|
}
|
|
func (gm *groupInMemory) UpdateGroupDocument(gid groupID, body []byte) error {
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
return errGroupNotExist
|
|
}
|
|
|
|
newbody, err := gd.updateBodyWithBson(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return gm.groupDocSync(gid, newbody)
|
|
}
|
|
|
|
func (cfg *groupConfig) prepareInMemory(ctx context.Context, region string, typename string, wsh *wshandler.WebsocketHandler) (group, error) {
|
|
// group document
|
|
// member document
|
|
groupDocSyncChanName := fmt.Sprintf("d_mgc_%s_%s", region, typename)
|
|
memberSyncChanName := fmt.Sprintf("m_mgc_%s_%s", region, typename)
|
|
rpcChanName := fmt.Sprintf("r_mgc_%s_%s", region, typename)
|
|
clientMessageChanName := fmt.Sprintf("c_mgc_%s_%s", region, typename)
|
|
|
|
toHashHex := func(name string) string {
|
|
hash := md5.New()
|
|
hash.Write([]byte(name))
|
|
if *flag.Devflag {
|
|
hn, _ := os.Hostname()
|
|
hash.Write([]byte(hn))
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)[:8])
|
|
}
|
|
|
|
groupDocSyncChanName = toHashHex(groupDocSyncChanName)
|
|
memberSyncChanName = toHashHex(memberSyncChanName)
|
|
rpcChanName = toHashHex(rpcChanName)
|
|
clientMessageChanName = toHashHex(clientMessageChanName)
|
|
|
|
// 여기서는 subscribe channel
|
|
// 각 함수에서는 publish
|
|
gm := &groupInMemory{
|
|
groupConfig: cfg,
|
|
groupDocSync: func(gid groupID, newbody []byte) error {
|
|
bt := []byte(fmt.Sprintf("%s%s", config.macAddr, gid.Hex()))
|
|
bt = append(bt, newbody...)
|
|
_, err := wsh.RedisSync.Publish(ctx, groupDocSyncChanName, bt).Result()
|
|
return err
|
|
},
|
|
memberSync: func(gid groupID, mid accountID, tid ticketID, doc *memberDoc, newmember bool) error {
|
|
var payload string
|
|
if doc != nil {
|
|
bt, _ := json.Marshal(doc)
|
|
newmemberflag := func() string {
|
|
if newmember {
|
|
return "t"
|
|
} else {
|
|
return "f"
|
|
}
|
|
}()
|
|
payload = fmt.Sprintf("%s%s%s%s%s%s", config.macAddr, gid.Hex(), mid.Hex(), tid.Hex(), newmemberflag, string(bt))
|
|
} else {
|
|
payload = fmt.Sprintf("%s%s%s%s", config.macAddr, gid.Hex(), mid.Hex(), tid.Hex())
|
|
}
|
|
_, err := wsh.RedisSync.Publish(ctx, memberSyncChanName, payload).Result()
|
|
return err
|
|
},
|
|
rpcCall: func(bt []byte) error {
|
|
_, err := wsh.RedisSync.Publish(ctx, rpcChanName, bt).Result()
|
|
return err
|
|
},
|
|
hasConn: func(t accountID) *wshandler.Richconn {
|
|
return wsh.Conn(region, t)
|
|
},
|
|
groups: groupContainer{
|
|
groupDocs: make(map[groupID]*groupDoc),
|
|
},
|
|
}
|
|
|
|
// TODO : processChannelMessage 스레드 분리해보자
|
|
processChannelMessage := func(gm *groupInMemory, pubsub *redis.PubSub) *redis.PubSub {
|
|
defer func() {
|
|
r := recover()
|
|
if r != nil {
|
|
logger.Error(r)
|
|
}
|
|
}()
|
|
|
|
for msg := range pubsub.Channel() {
|
|
if msg == nil {
|
|
pubsub = nil
|
|
break
|
|
}
|
|
|
|
switch msg.Channel {
|
|
case clientMessageChanName:
|
|
bt := []byte(msg.Payload)
|
|
if len(bt) < 24 {
|
|
break
|
|
}
|
|
|
|
// 주!! mid가 먼저
|
|
var mid groupID
|
|
copy(mid[:], bt[:12])
|
|
bt = bt[12:]
|
|
|
|
var gid groupID
|
|
copy(gid[:], bt[:12])
|
|
bt = bt[12:]
|
|
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
break
|
|
}
|
|
tid, _ := gd.memberByAccount(mid)
|
|
if !tid.IsZero() {
|
|
personalized := []byte(fmt.Sprintf(`{"%s":%s}`, tid.Hex(), string(bt)))
|
|
if after, err := gd.updateBodyWithJson(personalized); err == nil {
|
|
go multicast(gd.conns(false), after)
|
|
}
|
|
}
|
|
|
|
case groupDocSyncChanName:
|
|
payload := []byte(msg.Payload)
|
|
if len(payload) < len(config.macAddr) {
|
|
break
|
|
}
|
|
|
|
senderHost, remain := payload[:len(config.macAddr)], payload[len(config.macAddr):]
|
|
if len(remain) < 24 {
|
|
break
|
|
}
|
|
|
|
idstr, remain := remain[:24], remain[24:]
|
|
gid, _ := primitive.ObjectIDFromHex(string(idstr))
|
|
gd := gm.groups.find(gid)
|
|
if gd != nil {
|
|
if len(remain) == 0 {
|
|
// gid 그룹 삭제
|
|
// 그룹 안에 있는 멤버에게 알림
|
|
bt, _ := json.Marshal(makeTypeMessage(FullGroupDoc{
|
|
Gid: gid,
|
|
}))
|
|
go multicast(gd.conns(true), bt)
|
|
gm.groups.delete(gid)
|
|
} else if string(senderHost) != config.macAddr {
|
|
if r, err := gd.updateBodyBsonToJson(remain); err != nil {
|
|
logger.Error("groupDocSyncChanName message decode failed :", remain, err)
|
|
} else {
|
|
go multicast(gd.conns(true), r)
|
|
}
|
|
}
|
|
} else if string(senderHost) != config.macAddr {
|
|
var newDoc groupDoc
|
|
if err := newDoc.updateBody(remain); err != nil {
|
|
logger.Error("groupDocSyncChanName message decode failed :", remain, err)
|
|
} else {
|
|
gm.groups.add(gid, &newDoc)
|
|
}
|
|
}
|
|
|
|
case memberSyncChanName:
|
|
if len(msg.Payload) < len(config.macAddr) {
|
|
break
|
|
}
|
|
|
|
senderHost, remain := msg.Payload[:len(config.macAddr)], msg.Payload[len(config.macAddr):]
|
|
if len(remain) < 24 {
|
|
break
|
|
}
|
|
|
|
idstr, remain := remain[:24], remain[24:]
|
|
gid, _ := primitive.ObjectIDFromHex(idstr)
|
|
gd := gm.groups.find(gid)
|
|
if gd == nil {
|
|
// 미리 그룹을 없애고 싱크 메시지를 보낸후 받은 것일 수 있다.
|
|
break
|
|
}
|
|
|
|
idstr, remain = remain[:24], remain[24:]
|
|
mid, _ := primitive.ObjectIDFromHex(idstr)
|
|
idstr, remain = remain[:24], remain[24:]
|
|
tid, _ := primitive.ObjectIDFromHex(idstr)
|
|
|
|
isNewMember := false
|
|
if len(remain) > 0 {
|
|
idstr, remain = remain[:1], remain[1:]
|
|
isNewMember = idstr == "t"
|
|
}
|
|
|
|
var updated *memberDoc
|
|
rconn := wsh.Conn(region, mid)
|
|
|
|
if senderHost != config.macAddr {
|
|
// 내가 보낸 메시지가 아니면 멤버 도큐먼트 업데이트 하고 브로드캐스팅
|
|
if len(remain) == 0 {
|
|
// mid 삭제
|
|
gd.removeMember(mid, &tid)
|
|
updated = nil
|
|
} else {
|
|
updated = gd.overwriteMemberDocument(mid, &tid, []byte(remain))
|
|
}
|
|
} else {
|
|
updated = gd.member(tid)
|
|
}
|
|
|
|
if updated == nil {
|
|
// 멤버 삭제 알림
|
|
if rconn != nil {
|
|
// gid에 이미 다른 값이 있을 수 있다.
|
|
// 정확하게 이 값이면 제거하고, 아니면 넘어간다.
|
|
rconn.RemoveTag("gid", fmt.Sprintf("%s@%s", gid.Hex(), gm.Name))
|
|
}
|
|
broadcastTypedMessage(gm, gid, PublicMemberDoc{Tid: tid})
|
|
} else {
|
|
if isNewMember && updated.rconn == nil && rconn != nil {
|
|
updated.rconn = rconn
|
|
}
|
|
// 업데이트 된 플레이어(새로 들어온 플레이어 포함)를 모두에게 알려준다. 본인 포함, invitee 제외
|
|
broadcastTypedMessage(gm, gid, PublicMemberDoc{
|
|
Tid: tid,
|
|
memberDocCommon: updated.memberDocCommon,
|
|
})
|
|
}
|
|
|
|
if isNewMember {
|
|
if rconn != nil {
|
|
// 새 멤버이므로 기존 멤버를 다 보내준다.
|
|
rconn.AddTag("gid", fmt.Sprintf("%s@%s", gid.Hex(), gm.Name))
|
|
rconn.WriteBytes(gd.serializeFull(gid, clientMessageChanName))
|
|
}
|
|
}
|
|
|
|
case rpcChanName:
|
|
targetbt, fn, params, err := rpc.Decode[accountID]([]byte(msg.Payload))
|
|
if err != nil {
|
|
logger.Error("rpcChanName message decode failed :", msg.Payload, err)
|
|
break
|
|
}
|
|
|
|
call := func() {
|
|
method, ok := reflect.TypeOf(gm).MethodByName(fn)
|
|
if !ok {
|
|
logger.Printf("%s message decode failed :", targetbt, msg.Payload, err)
|
|
}
|
|
|
|
args := []reflect.Value{
|
|
reflect.ValueOf(gm),
|
|
}
|
|
for _, arg := range params {
|
|
args = append(args, reflect.ValueOf(arg))
|
|
}
|
|
|
|
method.Func.Call(args)
|
|
}
|
|
|
|
if *targetbt == everyHost {
|
|
call()
|
|
} else if rconn := wsh.Conn(region, *targetbt); rconn != nil {
|
|
call()
|
|
}
|
|
default:
|
|
logger.Println("unknown channel")
|
|
}
|
|
}
|
|
return pubsub
|
|
}
|
|
|
|
go func() {
|
|
defer func() {
|
|
r := recover()
|
|
if r != nil {
|
|
logger.Error(r)
|
|
}
|
|
}()
|
|
|
|
var pubsub *redis.PubSub
|
|
for {
|
|
if pubsub == nil {
|
|
pubsub = wsh.RedisSync.Subscribe(ctx, groupDocSyncChanName, memberSyncChanName, rpcChanName, clientMessageChanName)
|
|
}
|
|
|
|
if pubsub == nil {
|
|
time.Sleep(time.Second)
|
|
continue
|
|
}
|
|
pubsub = processChannelMessage(gm, pubsub)
|
|
}
|
|
}()
|
|
|
|
return gm, nil
|
|
}
|