491
core/friend.go
491
core/friend.go
@ -1,491 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"repositories.action2quare.com/ayo/gocommon"
|
||||
"repositories.action2quare.com/ayo/gocommon/logger"
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
)
|
||||
|
||||
const (
|
||||
friends_collection_name = gocommon.CollectionName("friends")
|
||||
monitoring_center_count = 100
|
||||
state_online = "online"
|
||||
state_offline = "offline"
|
||||
)
|
||||
|
||||
var friend_state_tag = []string{"social.FriendState"}
|
||||
|
||||
type friendDoc struct {
|
||||
Id primitive.ObjectID `bson:"_id,omitempty" json:"_id"`
|
||||
From primitive.ObjectID `bson:"from" json:"-"`
|
||||
To primitive.ObjectID `bson:"to" json:"-"`
|
||||
ToAlias string `bson:"talias" json:"to"`
|
||||
Timestamp int64 `bson:"ts" json:"ts"`
|
||||
Deleted bool `bson:"deleted,omitempty" json:"deleted,omitempty"`
|
||||
}
|
||||
|
||||
type registerListener struct {
|
||||
src primitive.ObjectID
|
||||
alias string
|
||||
l *listener
|
||||
}
|
||||
|
||||
type monitoringCenter struct {
|
||||
regChan chan registerListener
|
||||
publishState func(string, string, string)
|
||||
}
|
||||
|
||||
type connWithFriends struct {
|
||||
c *websocket.Conn
|
||||
friends []*friendDoc
|
||||
initialized bool
|
||||
}
|
||||
|
||||
type connections struct {
|
||||
connLock sync.Mutex
|
||||
conns map[primitive.ObjectID]*connWithFriends
|
||||
}
|
||||
|
||||
func (cs *connections) new(accid primitive.ObjectID, conn *websocket.Conn) {
|
||||
cs.connLock.Lock()
|
||||
defer cs.connLock.Unlock()
|
||||
cs.conns[accid] = &connWithFriends{
|
||||
c: conn,
|
||||
initialized: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *connections) delete(accid primitive.ObjectID) {
|
||||
cs.connLock.Lock()
|
||||
defer cs.connLock.Unlock()
|
||||
|
||||
delete(cs.conns, accid)
|
||||
}
|
||||
|
||||
func (cs *connections) conn(accid primitive.ObjectID) *websocket.Conn {
|
||||
cs.connLock.Lock()
|
||||
defer cs.connLock.Unlock()
|
||||
if cf, ok := cs.conns[accid]; ok {
|
||||
return cf.c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *connections) addFriend(accid primitive.ObjectID, fdoc *friendDoc) bool {
|
||||
cs.connLock.Lock()
|
||||
defer cs.connLock.Unlock()
|
||||
if cf, ok := cs.conns[accid]; ok {
|
||||
if cf.initialized {
|
||||
cf.friends = append(cf.friends, fdoc)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (cs *connections) initFriends(accid primitive.ObjectID, fdocs []*friendDoc) {
|
||||
cs.connLock.Lock()
|
||||
defer cs.connLock.Unlock()
|
||||
if cf, ok := cs.conns[accid]; ok {
|
||||
cf.friends = fdocs
|
||||
cf.initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *connections) clearFriends(accid primitive.ObjectID) (out []*friendDoc) {
|
||||
cs.connLock.Lock()
|
||||
defer cs.connLock.Unlock()
|
||||
if cf, ok := cs.conns[accid]; ok {
|
||||
out = cf.friends
|
||||
cf.friends = nil
|
||||
cf.initialized = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type friends struct {
|
||||
mongoClient gocommon.MongoClient
|
||||
redison *gocommon.RedisonHandler
|
||||
wsh *wshandler.WebsocketHandler
|
||||
moncen []monitoringCenter
|
||||
conns connections
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
c *websocket.Conn
|
||||
me primitive.ObjectID
|
||||
}
|
||||
|
||||
type listenerMap struct {
|
||||
listeners map[primitive.ObjectID]*listener
|
||||
connected bool
|
||||
online []byte
|
||||
offline []byte
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]friendDoc{})
|
||||
}
|
||||
|
||||
// per channel
|
||||
// src(alias) - listener(objectid) : socket
|
||||
// - listener(objectid) : socket
|
||||
// - listener(objectid) : socket
|
||||
|
||||
func makeSrcMap(src string, connected bool) *listenerMap {
|
||||
online, _ := json.Marshal(wshandler.DownstreamMessage{
|
||||
Body: bson.M{
|
||||
"from": src,
|
||||
"state": state_online,
|
||||
},
|
||||
Tag: friend_state_tag,
|
||||
})
|
||||
|
||||
offline, _ := json.Marshal(wshandler.DownstreamMessage{
|
||||
Body: bson.M{
|
||||
"from": src,
|
||||
"state": state_offline,
|
||||
},
|
||||
Tag: friend_state_tag,
|
||||
})
|
||||
|
||||
return &listenerMap{
|
||||
listeners: make(map[primitive.ObjectID]*listener),
|
||||
connected: connected,
|
||||
online: online,
|
||||
offline: offline,
|
||||
}
|
||||
}
|
||||
|
||||
func makeFriends(ctx context.Context, so *Social) (*friends, error) {
|
||||
if err := so.mongoClient.MakeUniqueIndices(friends_collection_name, map[string]bson.D{
|
||||
"fromto": {{Key: "from", Value: 1}, {Key: "to", Value: 1}},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var moncen []monitoringCenter
|
||||
for i := 0; i < monitoring_center_count; i++ {
|
||||
subChannel := fmt.Sprintf("_soc_fr_monitor_ch_%d_%d", i, so.redison.Options().DB)
|
||||
regChan := make(chan registerListener)
|
||||
moncen = append(moncen, monitoringCenter{
|
||||
regChan: regChan,
|
||||
publishState: func(src, alias, state string) {
|
||||
so.redison.Publish(ctx, subChannel, src+alias+":"+state).Result()
|
||||
},
|
||||
})
|
||||
|
||||
go func(subChannel string, regChan chan registerListener) {
|
||||
pubsub := so.redison.Subscribe(ctx, subChannel)
|
||||
listeners := make(map[primitive.ObjectID]*listenerMap)
|
||||
for {
|
||||
select {
|
||||
case reg := <-regChan:
|
||||
// 내가 관심있는 애들 등록
|
||||
srcmap, online := listeners[reg.src]
|
||||
if !online {
|
||||
srcmap = makeSrcMap(reg.alias, false)
|
||||
listeners[reg.src] = srcmap
|
||||
}
|
||||
|
||||
if reg.l.c == nil {
|
||||
// 등록 해제. 모니터링 종료
|
||||
// listener목록에서 나(reg.l.me)를 제거
|
||||
delete(srcmap.listeners, reg.l.me)
|
||||
online = false
|
||||
logger.Println("regChan unregistered :", reg.src.Hex(), reg.l.me.Hex())
|
||||
} else if oldl, ok := srcmap.listeners[reg.l.me]; ok {
|
||||
// 내가 이미 리스너로 등록되어 있다.
|
||||
// 상대방이 나를 차단했을 경우에는 기존 리스너가 nil임
|
||||
online = oldl != nil
|
||||
logger.Println("regChan registered :", reg.src.Hex(), reg.l.me.Hex(), "old", online)
|
||||
} else {
|
||||
logger.Println("regChan registered :", reg.src.Hex(), reg.l.me.Hex())
|
||||
srcmap.listeners[reg.l.me] = reg.l
|
||||
}
|
||||
|
||||
if online && srcmap != nil {
|
||||
logger.Println("regChan send online :", reg.l.me.Hex(), string(srcmap.online))
|
||||
reg.l.c.WriteMessage(websocket.TextMessage, srcmap.online)
|
||||
}
|
||||
|
||||
if len(srcmap.listeners) == 0 && !srcmap.connected {
|
||||
delete(listeners, reg.src)
|
||||
}
|
||||
|
||||
case msg := <-pubsub.Channel():
|
||||
target, _ := primitive.ObjectIDFromHex(msg.Payload[:24])
|
||||
aliasstate := strings.SplitN(msg.Payload[24:], ":", 2)
|
||||
var sent []byte
|
||||
if srcmap, ok := listeners[target]; ok {
|
||||
if aliasstate[1] == state_online {
|
||||
sent = srcmap.online
|
||||
srcmap.connected = true
|
||||
} else if aliasstate[1] == state_offline {
|
||||
sent = srcmap.offline
|
||||
srcmap.connected = false
|
||||
if len(srcmap.listeners) == 0 {
|
||||
delete(listeners, target)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sent) > 0 {
|
||||
for _, l := range srcmap.listeners {
|
||||
logger.Println("state fire :", l.me, string(sent))
|
||||
l.c.WriteMessage(websocket.TextMessage, sent)
|
||||
}
|
||||
}
|
||||
} else if aliasstate[1] == state_online {
|
||||
listeners[target] = makeSrcMap(aliasstate[0], true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(subChannel, regChan)
|
||||
}
|
||||
|
||||
return &friends{
|
||||
mongoClient: so.mongoClient,
|
||||
redison: so.redison,
|
||||
wsh: so.wsh,
|
||||
moncen: moncen,
|
||||
conns: connections{
|
||||
conns: make(map[primitive.ObjectID]*connWithFriends),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fs *friends) ClientConnected(conn *websocket.Conn, callby *wshandler.Sender) {
|
||||
fs.conns.new(callby.Accid, conn)
|
||||
|
||||
// 내 로그인 상태를 알림
|
||||
meidx := callby.Accid[11] % monitoring_center_count
|
||||
fs.moncen[meidx].publishState(callby.Accid.Hex(), callby.Alias, state_online)
|
||||
}
|
||||
|
||||
func (fs *friends) ClientDisconnected(conn *websocket.Conn, callby *wshandler.Sender) {
|
||||
// 로그 오프 상태를 알림
|
||||
meidx := callby.Accid[11] % monitoring_center_count
|
||||
fs.moncen[meidx].publishState(callby.Accid.Hex(), callby.Alias, state_offline)
|
||||
|
||||
fs.stopMonitoringFriends(callby.Accid)
|
||||
|
||||
fs.conns.delete(callby.Accid)
|
||||
}
|
||||
|
||||
func (fs *friends) writeMessage(acc primitive.ObjectID, src any) {
|
||||
c := fs.conns.conn(acc)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if bt, err := json.Marshal(src); err == nil {
|
||||
c.WriteMessage(websocket.TextMessage, bt)
|
||||
}
|
||||
}
|
||||
|
||||
var errAddFriendFailed = errors.New("addFriend failed")
|
||||
|
||||
func (fs *friends) addFriend(f *friendDoc) error {
|
||||
_, newid, err := fs.mongoClient.Update(friends_collection_name, bson.M{
|
||||
"_id": primitive.NewObjectID(),
|
||||
}, bson.M{
|
||||
"$setOnInsert": f,
|
||||
}, options.Update().SetUpsert(true))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newid == nil {
|
||||
return errAddFriendFailed
|
||||
}
|
||||
|
||||
f.Id = newid.(primitive.ObjectID)
|
||||
if fs.conns.addFriend(f.From, f) {
|
||||
// 모니터링 중
|
||||
conn := fs.conns.conn(f.From)
|
||||
if conn != nil {
|
||||
toidx := f.To[11] % monitoring_center_count
|
||||
fs.moncen[toidx].regChan <- registerListener{
|
||||
src: f.To,
|
||||
alias: f.ToAlias,
|
||||
l: &listener{
|
||||
c: conn,
|
||||
me: f.From,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *friends) Block(ctx wshandler.ApiCallContext) {
|
||||
// BlockByMe 에 추가하고 상대의 BlockByYou를 설정한다.
|
||||
|
||||
// var bi struct {
|
||||
// From primitive.ObjectID
|
||||
// To primitive.ObjectID
|
||||
// }
|
||||
// if err := gocommon.MakeDecoder(r).Decode(&bi); err != nil {
|
||||
// logger.Println("friends.Block failed :", err)
|
||||
// w.WriteHeader(http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
// logger.Println("friends.Block :", bi)
|
||||
}
|
||||
|
||||
func (fs *friends) DeleteFriend(ctx wshandler.ApiCallContext) {
|
||||
fid, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string))
|
||||
|
||||
var fdoc friendDoc
|
||||
if err := fs.mongoClient.FindOneAs(friends_collection_name, bson.M{
|
||||
"_id": fid,
|
||||
}, &fdoc, options.FindOne().SetProjection(bson.M{
|
||||
"from": 1,
|
||||
"to": 1,
|
||||
})); err != nil {
|
||||
logger.Println("DeleteFriend is failed :", err)
|
||||
return
|
||||
}
|
||||
|
||||
if fdoc.Id.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Unix()
|
||||
fdoc.Deleted = true
|
||||
fdoc.Timestamp = now
|
||||
|
||||
// 나한테 삭제
|
||||
fs.mongoClient.Update(friends_collection_name, bson.M{
|
||||
"_id": fid,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
"deleted": true,
|
||||
"ts": fdoc.Timestamp,
|
||||
},
|
||||
}, options.Update().SetUpsert(false))
|
||||
fs.writeMessage(ctx.CallBy.Accid, &wshandler.DownstreamMessage{
|
||||
Body: []friendDoc{fdoc},
|
||||
Tag: friends_tag,
|
||||
})
|
||||
|
||||
// 상대방에게 삭제
|
||||
var yourdoc friendDoc
|
||||
if err := fs.mongoClient.FindOneAndUpdateAs(friends_collection_name, bson.M{
|
||||
"from": fdoc.To,
|
||||
"to": fdoc.From,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
"deleted": true,
|
||||
"ts": now,
|
||||
},
|
||||
}, &yourdoc, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(false)); err == nil {
|
||||
fs.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: fdoc.To.Hex(),
|
||||
Body: []friendDoc{yourdoc},
|
||||
Tag: friends_tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *friends) StartMonitoringFriends(ctx wshandler.ApiCallContext) {
|
||||
// 내 친구 목록에 나를 등록
|
||||
var friends []*friendDoc
|
||||
if err := fs.mongoClient.FindAllAs(friends_collection_name, bson.M{
|
||||
"from": ctx.CallBy.Accid,
|
||||
}, &friends, options.Find().SetProjection(bson.M{"to": 1, "talias": 1})); err != nil {
|
||||
logger.Println("StartMonitoringFriends is failed :", err)
|
||||
return
|
||||
}
|
||||
|
||||
me := &listener{
|
||||
c: fs.conns.conn(ctx.CallBy.Accid),
|
||||
me: ctx.CallBy.Accid,
|
||||
}
|
||||
|
||||
for _, f := range friends {
|
||||
toidx := f.To[11] % monitoring_center_count
|
||||
fs.moncen[toidx].regChan <- registerListener{
|
||||
src: f.To,
|
||||
alias: f.ToAlias,
|
||||
l: me,
|
||||
}
|
||||
}
|
||||
|
||||
fs.conns.initFriends(ctx.CallBy.Accid, friends)
|
||||
}
|
||||
|
||||
func (fs *friends) stopMonitoringFriends(accid primitive.ObjectID) {
|
||||
friends := fs.conns.clearFriends(accid)
|
||||
|
||||
if len(friends) > 0 {
|
||||
// 나를 상대방 모니터링에서 뺀다
|
||||
nilListener := &listener{c: nil, me: accid}
|
||||
for _, f := range friends {
|
||||
toidx := f.To[11] % monitoring_center_count
|
||||
fs.moncen[toidx].regChan <- registerListener{
|
||||
src: f.To,
|
||||
alias: f.ToAlias,
|
||||
l: nilListener,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *friends) StopMonitoringFriends(ctx wshandler.ApiCallContext) {
|
||||
fs.stopMonitoringFriends(ctx.CallBy.Accid)
|
||||
}
|
||||
|
||||
func (fs *friends) QueryFriends(ctx wshandler.ApiCallContext) {
|
||||
queryfrom := int64(ctx.Arguments[0].(float64))
|
||||
|
||||
var myfriends []friendDoc
|
||||
err := fs.mongoClient.FindAllAs(friends_collection_name, bson.M{
|
||||
"from": ctx.CallBy.Accid,
|
||||
"ts": bson.M{"$gt": queryfrom},
|
||||
}, &myfriends)
|
||||
if err != nil {
|
||||
logger.Println("QueryReceivedInvitations failed. FindAllAs err :", err)
|
||||
}
|
||||
|
||||
if len(myfriends) > 0 {
|
||||
fs.writeMessage(ctx.CallBy.Accid, &wshandler.DownstreamMessage{
|
||||
Alias: ctx.CallBy.Alias,
|
||||
Body: myfriends,
|
||||
Tag: friends_tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *friends) Trim(ctx wshandler.ApiCallContext) {
|
||||
stringsTobjs := func(in []any) (out []primitive.ObjectID) {
|
||||
for _, i := range in {
|
||||
p, _ := primitive.ObjectIDFromHex(i.(string))
|
||||
out = append(out, p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ids := stringsTobjs(ctx.Arguments[2].([]any))
|
||||
if len(ids) > 0 {
|
||||
if len(ids) == 1 {
|
||||
fs.mongoClient.Delete(friends_collection_name, bson.M{"_id": ids[0]})
|
||||
} else {
|
||||
fs.mongoClient.DeleteMany(friends_collection_name, bson.D{{Key: "_id", Value: bson.M{"$in": ids}}})
|
||||
}
|
||||
}
|
||||
}
|
||||
3
core/group.go
Normal file
3
core/group.go
Normal file
@ -0,0 +1,3 @@
|
||||
package core
|
||||
|
||||
type configDocument map[string]any
|
||||
380
core/group_chat.go
Normal file
380
core/group_chat.go
Normal file
@ -0,0 +1,380 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"repositories.action2quare.com/ayo/gocommon"
|
||||
"repositories.action2quare.com/ayo/gocommon/logger"
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
)
|
||||
|
||||
type channelID = string
|
||||
type channelConfig struct {
|
||||
Capacity int64 `json:"capacity"`
|
||||
Size int64 `json:"size"`
|
||||
Key string `json:"key"`
|
||||
emptyJson string
|
||||
}
|
||||
|
||||
type chatConfig struct {
|
||||
DefaultCapacity int64 `json:"default_capacity"`
|
||||
Channels map[string]*channelConfig `json:"channels"`
|
||||
}
|
||||
|
||||
type groupChat struct {
|
||||
chatConfig
|
||||
rh *gocommon.RedisonHandler
|
||||
enterRoom func(channelID, accountID)
|
||||
leaveRoom func(channelID, accountID)
|
||||
sendUpstreamMessage func(msg *wshandler.UpstreamMessage)
|
||||
}
|
||||
|
||||
func (gc *groupChat) Initialize(tv *Tavern, cfg configDocument) error {
|
||||
rem, _ := json.Marshal(cfg)
|
||||
if err := json.Unmarshal(rem, &gc.chatConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gc.enterRoom = func(chanid channelID, accid accountID) {
|
||||
tv.wsh.EnterRoom(string(chanid), accid)
|
||||
}
|
||||
gc.leaveRoom = func(chanid channelID, accid accountID) {
|
||||
tv.wsh.LeaveRoom(string(chanid), accid)
|
||||
}
|
||||
gc.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) {
|
||||
tv.wsh.SendUpstreamMessage(msg)
|
||||
}
|
||||
|
||||
gc.rh = tv.redison
|
||||
|
||||
for name, cfg := range gc.chatConfig.Channels {
|
||||
if cfg.Capacity == 0 {
|
||||
cfg.Capacity = gc.chatConfig.DefaultCapacity
|
||||
}
|
||||
cfg.Key = name
|
||||
cfg.Size = 0
|
||||
|
||||
jm, _ := json.Marshal(cfg)
|
||||
cfg.emptyJson = fmt.Sprintf("[%s]", string(jm))
|
||||
|
||||
_, err := gc.rh.JSONSet(name, "$", cfg)
|
||||
if *devflag && err != nil {
|
||||
gc.rh.JSONDel(name, "$")
|
||||
_, err = gc.rh.JSONSet(name, "$", cfg)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gc *groupChat) ClientConnected(ctx wshandler.ApiCallContext) {
|
||||
gc.rh.JSONSet(ctx.CallBy.Accid.Hex(), "$.channel", map[string]any{})
|
||||
}
|
||||
|
||||
func (gc *groupChat) ClientDisconnected(ctx wshandler.ApiCallContext) {
|
||||
docs, _ := gc.rh.JSONGetDocuments(ctx.CallBy.Accid.Hex(), "$.channel")
|
||||
|
||||
if len(docs) > 0 {
|
||||
for k, v := range docs[0] {
|
||||
typename := k
|
||||
chanid := v.(string)
|
||||
gc.leaveRoom(chanid, ctx.CallBy.Accid)
|
||||
if k == "public" {
|
||||
gc.rh.JSONNumIncrBy(chanid, "$.size", -1)
|
||||
} else {
|
||||
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + chanid,
|
||||
Body: map[string]any{"sender": ctx.CallBy.Alias, "typename": typename},
|
||||
Tag: []string{"LeavePrivateChannel"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gc *groupChat) EnterPublicChannel(ctx wshandler.ApiCallContext) {
|
||||
chanid := ctx.Arguments[0].(string)
|
||||
if cfg, ok := gc.chatConfig.Channels[chanid]; ok {
|
||||
size, err := gc.rh.JSONGetInt64(chanid, "$.size")
|
||||
if err != nil || len(size) == 0 {
|
||||
logger.Println("JSONGetInt64 failed :", chanid, err)
|
||||
} else if size[0] < cfg.Capacity {
|
||||
// 입장
|
||||
newsize, err := gc.rh.JSONNumIncrBy(chanid, "$.size", 1)
|
||||
if err == nil {
|
||||
gc.enterRoom(chanid, ctx.CallBy.Accid)
|
||||
gc.rh.JSONSet(ctx.CallBy.Accid.Hex(), "$.channel.public", chanid)
|
||||
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + chanid,
|
||||
Body: map[string]any{"size": newsize[0]},
|
||||
Tag: []string{"ChattingChannelProperties"},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 풀방
|
||||
logger.Println("chatting channel is full :", chanid, size, cfg.Capacity)
|
||||
}
|
||||
} else {
|
||||
logger.Println("chatting channel not valid :", chanid)
|
||||
}
|
||||
}
|
||||
|
||||
func (gc *groupChat) LeavePublicChannel(ctx wshandler.ApiCallContext) {
|
||||
chanid := ctx.Arguments[0].(string)
|
||||
cnt, _ := gc.rh.JSONDel(ctx.CallBy.Accid.Hex(), "$.channel.public")
|
||||
if cnt > 0 {
|
||||
gc.leaveRoom(chanid, ctx.CallBy.Accid)
|
||||
gc.rh.JSONNumIncrBy(chanid, "$.size", -1)
|
||||
}
|
||||
}
|
||||
|
||||
func (gc *groupChat) TextMessage(ctx wshandler.ApiCallContext) {
|
||||
chanid := ctx.Arguments[0].(string)
|
||||
msg := ctx.Arguments[1].(string)
|
||||
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + chanid,
|
||||
Body: map[string]any{"sender": ctx.CallBy.Alias, "msg": msg},
|
||||
Tag: []string{"TextMessage"},
|
||||
})
|
||||
}
|
||||
|
||||
func (gc *groupChat) EnterPrivateChannel(ctx wshandler.ApiCallContext) {
|
||||
typename := ctx.Arguments[0].(string)
|
||||
channel := ctx.Arguments[1].(string)
|
||||
var reason string
|
||||
if len(ctx.Arguments) > 2 {
|
||||
reason = ctx.Arguments[2].(string)
|
||||
}
|
||||
|
||||
if len(reason) > 0 {
|
||||
// 수락
|
||||
ok, err := gc.rh.JSONSet(ctx.CallBy.Accid.Hex(), "$.channel."+typename, channel, gocommon.RedisonSetOptionNX)
|
||||
if err != nil || !ok {
|
||||
// 이미 다른 private channel 참여 중
|
||||
logger.Println("EnterPrivateChannel failed. HSetNX return err :", err, ctx.CallBy.Accid.Hex(), typename, channel)
|
||||
return
|
||||
}
|
||||
gc.enterRoom(channel, ctx.CallBy.Accid)
|
||||
}
|
||||
|
||||
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + channel,
|
||||
Body: map[string]any{
|
||||
"sender": ctx.CallBy.Alias,
|
||||
"msg": reason,
|
||||
"typename": typename,
|
||||
},
|
||||
Tag: []string{"EnterPrivateChannel"},
|
||||
})
|
||||
}
|
||||
|
||||
func (gc *groupChat) LeavePrivateChannel(ctx wshandler.ApiCallContext) {
|
||||
typename := ctx.Arguments[0].(string)
|
||||
chanid := ctx.Arguments[1].(string)
|
||||
cnt, _ := gc.rh.JSONDel(ctx.CallBy.Accid.Hex(), "$.channel."+typename)
|
||||
if cnt > 0 {
|
||||
gc.leaveRoom(chanid, ctx.CallBy.Accid)
|
||||
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + chanid,
|
||||
Body: map[string]any{"sender": ctx.CallBy.Alias, "typename": typename},
|
||||
Tag: []string{"LeavePrivateChannel"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// func (gc *groupChat) ClientMessageReceived(sender *wshandler.Sender, mt wshandler.WebSocketMessageType, message any) {
|
||||
// if mt == wshandler.Disconnected {
|
||||
// if _, err := gc.rh.Del(gc.rh.Context(), accidHex(sender.Accid)).Result(); err != nil {
|
||||
// logger.Println(err)
|
||||
// }
|
||||
// } else if mt == wshandler.BinaryMessage {
|
||||
// commandline := message.([]any)
|
||||
// cmd := commandline[0].(string)
|
||||
// args := commandline[1:]
|
||||
// switch cmd {
|
||||
// case "EnterPublicChannel":
|
||||
// chanid := args[0].(string)
|
||||
// if cfg, ok := gc.chatConfig.Channels[chanid]; ok {
|
||||
// size, err := gc.rh.JSONGetInt64(chanid, "$.size")
|
||||
// if err != nil || len(size) == 0 {
|
||||
// logger.Println("JSONGetInt64 failed :", chanid, err)
|
||||
// } else if size[0] < cfg.Capacity {
|
||||
// // 입장
|
||||
// newsize, err := gc.rh.JSONNumIncrBy(chanid, "$.size", 1)
|
||||
// if err == nil {
|
||||
// gc.enterRoom(chanid, sender.Accid)
|
||||
// sender.RegistDisconnectedCallback(chanid, func() {
|
||||
// size, err := gc.rh.JSONNumIncrBy(chanid, "$.size", -1)
|
||||
// if err == nil {
|
||||
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
// Target: "#" + chanid,
|
||||
// Body: map[string]any{"size": size},
|
||||
// Tag: []string{"ChattingChannelProperties"},
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// gc.rh.HSet(gc.rh.Context(), accidHex(sender.Accid), "cc_pub", chanid)
|
||||
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
// Target: "#" + chanid,
|
||||
// Body: map[string]any{"size": newsize[0]},
|
||||
// Tag: []string{"ChattingChannelProperties"},
|
||||
// })
|
||||
// }
|
||||
// } else {
|
||||
// // 풀방
|
||||
// logger.Println("chatting channel is full :", chanid, size, cfg.Capacity)
|
||||
// }
|
||||
// } else {
|
||||
// logger.Println("chatting channel not valid :", chanid)
|
||||
// }
|
||||
|
||||
// case "LeavePublicChannel":
|
||||
// chanid := args[0].(string)
|
||||
// gc.rh.HDel(gc.rh.Context(), accidHex(sender.Accid), "cc_pub")
|
||||
// gc.leaveRoom(chanid, sender.Accid)
|
||||
// if f := sender.PopDisconnectedCallback(chanid); f != nil {
|
||||
// f()
|
||||
// }
|
||||
|
||||
// case "TextMessage":
|
||||
// chanid := args[0].(string)
|
||||
// msg := args[1].(string)
|
||||
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
// Target: "#" + chanid,
|
||||
// Body: map[string]any{"sender": sender.Alias, "msg": msg},
|
||||
// Tag: []string{"TextMessage"},
|
||||
// })
|
||||
|
||||
// case "EnterPrivateChannel":
|
||||
// typename := args[0].(string)
|
||||
// channel := args[1].(string)
|
||||
// var reason string
|
||||
// if len(args) > 2 {
|
||||
// reason = args[2].(string)
|
||||
// }
|
||||
|
||||
// if len(reason) > 0 {
|
||||
// // 수락
|
||||
// // 이거 HSet 하면 안되겠는데? JSONSet해야할 듯?
|
||||
// ok, err := gc.rh.HSetNX(gc.rh.Context(), accidHex(sender.Accid), "cc_"+typename, channel).Result()
|
||||
// if err != nil || !ok {
|
||||
// // 이미 다른 private channel 참여 중
|
||||
// logger.Println("EnterPrivateChannel failed. HSetNX return err :", err, sender.Accid.Hex(), typename, channel)
|
||||
// return
|
||||
// }
|
||||
// gc.enterRoom(channel, sender.Accid)
|
||||
|
||||
// sender.RegistDisconnectedCallback(channel, func() {
|
||||
// gc.rh.JSONDel(channel, "$."+sender.Accid.Hex())
|
||||
// // 이거 HDel 하면 안되겠는데? JSONDel해야할 듯?
|
||||
// cnt, _ := gc.rh.HDel(gc.rh.Context(), accidHex(sender.Accid), "cc_"+typename).Result()
|
||||
// if cnt > 0 {
|
||||
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
// Target: "#" + channel,
|
||||
// Body: map[string]any{"sender": sender.Alias, "typename": typename},
|
||||
// Tag: []string{"LeavePrivateChannel"},
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
// // 내가 이미 private channel에 있다는 것을 다른 사람들에게 알려주기 위함
|
||||
// }
|
||||
|
||||
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
// Target: "#" + channel,
|
||||
// Body: map[string]any{
|
||||
// "sender": sender.Alias,
|
||||
// "msg": reason,
|
||||
// "typename": typename,
|
||||
// },
|
||||
// Tag: []string{"EnterPrivateChannel"},
|
||||
// })
|
||||
|
||||
// case "LeavePrivateChannel":
|
||||
// channel := args[1].(string)
|
||||
// gc.leaveRoom(channel, sender.Accid)
|
||||
// if f := sender.PopDisconnectedCallback(channel); f != nil {
|
||||
// f()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
func (gc *groupChat) FetchChattingChannels(w http.ResponseWriter, r *http.Request) {
|
||||
var prefix string
|
||||
if err := gocommon.MakeDecoder(r).Decode(&prefix); err != nil {
|
||||
logger.Println("FetchChattingChannels failed. ReadJsonDocumentFromBody returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(prefix) == 0 {
|
||||
logger.Println("FetchChattingChannel failed. prefix is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var rows []string
|
||||
for name, cfg := range gc.chatConfig.Channels {
|
||||
if len(prefix) > 0 {
|
||||
if !strings.HasPrefix(name, prefix) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
onechan, err := gc.rh.JSONGet(name, "$")
|
||||
if err != nil && err != redis.Nil {
|
||||
logger.Println("FetchChattingChannel failed. HGetAll return err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err == redis.Nil || onechan == nil {
|
||||
rows = append(rows, cfg.emptyJson)
|
||||
} else {
|
||||
// json array로 나온다
|
||||
rows = append(rows, strings.Trim(onechan.(string), "[]"))
|
||||
}
|
||||
}
|
||||
|
||||
gocommon.MakeEncoder(w, r).Encode(rows)
|
||||
}
|
||||
|
||||
func (gc *groupChat) QueryPlayerChattingChannel(w http.ResponseWriter, r *http.Request) {
|
||||
var accid primitive.ObjectID
|
||||
if err := gocommon.MakeDecoder(r).Decode(&accid); err != nil {
|
||||
logger.Println("QueryPlayerChattingChannel failed. ReadJsonDocumentFromBody returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sub, err := gc.rh.JSONGetDocuments(accid.Hex(), "$.channel")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(sub) > 0 {
|
||||
gocommon.MakeEncoder(w, r).Encode(sub[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (gc *groupChat) SendMessageOnChannel(w http.ResponseWriter, r *http.Request) {
|
||||
var msg wshandler.UpstreamMessage
|
||||
if err := gocommon.MakeDecoder(r).Decode(&msg); err != nil {
|
||||
logger.Println("SendMessageOnChannel failed. ReadJsonDocumentFromBody return err :", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
gc.sendUpstreamMessage(&msg)
|
||||
}
|
||||
822
core/group_party.go
Normal file
822
core/group_party.go
Normal file
@ -0,0 +1,822 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"repositories.action2quare.com/ayo/gocommon"
|
||||
"repositories.action2quare.com/ayo/gocommon/logger"
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type accountID = primitive.ObjectID
|
||||
type groupID = primitive.ObjectID
|
||||
|
||||
func makeTid(gid groupID, in accountID) string {
|
||||
var out primitive.ObjectID
|
||||
for i := range in {
|
||||
out[12-i-1] = gid[i] ^ in[12-i-1]
|
||||
}
|
||||
return out.Hex()
|
||||
}
|
||||
|
||||
func midFromTid(gid groupID, in string) accountID {
|
||||
h, _ := primitive.ObjectIDFromHex(in)
|
||||
|
||||
var out accountID
|
||||
for i := range h {
|
||||
out[12-i-1] = gid[i] ^ h[12-i-1]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type Invitation struct {
|
||||
GroupID groupID `json:"_gid"`
|
||||
TicketID string `json:"_tid"`
|
||||
Inviter bson.M `json:"_inviter"` // memberDoc.Body
|
||||
ExpireAtUTC int64 `json:"_expire_at_utc"`
|
||||
}
|
||||
|
||||
// 플레이어한테 공유하는 멤버 정보
|
||||
type memberDoc struct {
|
||||
Body bson.M `json:"_body"`
|
||||
Invite bool `json:"_invite"`
|
||||
InviteExpire int64 `json:"_invite_exp"`
|
||||
}
|
||||
|
||||
type InvitationFail bson.M
|
||||
|
||||
type 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(mid accountID, body bson.M, ttl time.Duration, max int) (*memberDoc, error) {
|
||||
targetmid := mid
|
||||
targetbody := body
|
||||
|
||||
// 초대 가능한 빈 자리가 있나
|
||||
tids, err := gd.rh.JSONObjKeys(gd.strid(), "$._members")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
createNewDoc := func() *memberDoc {
|
||||
return &memberDoc{
|
||||
Body: targetbody,
|
||||
Invite: true,
|
||||
InviteExpire: now.Add(ttl).Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
newtid := gd.tid(targetmid)
|
||||
if len(tids) < max {
|
||||
// 빈자리를 찾았다.
|
||||
newdoc := createNewDoc()
|
||||
_, err := gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
|
||||
return newdoc, err
|
||||
}
|
||||
|
||||
expires, err := gd.rh.JSONGetInt64(gd.strid(), "$._members.._invite_exp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var delpaths []string
|
||||
for i, expire := range expires {
|
||||
if expire < now.Unix() {
|
||||
// 만료된 초대가 있네? 지우자
|
||||
delpaths = append(delpaths, "$._members."+tids[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(delpaths) == 0 {
|
||||
// 빈자리가 없다
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := gd.rh.JSONMDel(gd.strid(), delpaths); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newdoc := createNewDoc()
|
||||
_, err = gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
|
||||
return newdoc, err
|
||||
}
|
||||
|
||||
func (gd *groupDoc) addMember(mid accountID, character bson.M) (bson.M, error) {
|
||||
tid := gd.tid(mid)
|
||||
prefix := "$._members." + tid
|
||||
|
||||
if _, err := gd.rh.JSONSet(gd.strid(), prefix+"._body", character, gocommon.RedisonSetOptionXX); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := gd.rh.JSONMDel(gd.strid(), []string{prefix + "._invite", prefix + "._invite_exp"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gd.rh.Persist(gd.rh.Context(), gd.strid()).Result()
|
||||
|
||||
return gd.loadMemberFull(tid)
|
||||
}
|
||||
|
||||
func (gd *groupDoc) removeMemberByTid(tid string) error {
|
||||
_, err := gd.rh.JSONDel(gd.strid(), "$._members."+tid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
counts, err := gd.rh.JSONObjLen(gd.strid(), "$._members")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(counts) > 0 && counts[0] == 0 {
|
||||
_, err = gd.rh.Del(gd.rh.Context(), gd.strid()).Result()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (gd *groupDoc) removeMember(mid accountID) error {
|
||||
return gd.removeMemberByTid(gd.tid(mid))
|
||||
}
|
||||
|
||||
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)
|
||||
enterRoom func(groupID, accountID)
|
||||
leaveRoom func(groupID, accountID)
|
||||
rh *gocommon.RedisonHandler
|
||||
}
|
||||
|
||||
func (gp *groupParty) Initialize(tv *Tavern, cfg configDocument) error {
|
||||
rem, _ := json.Marshal(cfg)
|
||||
err := json.Unmarshal(rem, &gp.partyConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gp.rh = tv.redison
|
||||
gp.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) {
|
||||
tv.wsh.SendUpstreamMessage(msg)
|
||||
}
|
||||
gp.enterRoom = func(gid groupID, accid accountID) {
|
||||
tv.wsh.EnterRoom(gid.Hex(), accid)
|
||||
}
|
||||
gp.leaveRoom = func(gid groupID, accid accountID) {
|
||||
tv.wsh.LeaveRoom(gid.Hex(), accid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gp *groupParty) RegisterApiFunctions() {
|
||||
|
||||
}
|
||||
|
||||
// JoinParty : 그룹에 참가
|
||||
// - type : 그룹 타입
|
||||
// - 그룹 타입에 맞는 키(주로 _id)
|
||||
// - member_id : 참가 멤버의 아이디
|
||||
// - body : 멤버의 속성 bson document
|
||||
func (gp *groupParty) JoinParty(w http.ResponseWriter, r *http.Request) {
|
||||
var data struct {
|
||||
Gid primitive.ObjectID
|
||||
Mid primitive.ObjectID
|
||||
First bool
|
||||
Character bson.M
|
||||
}
|
||||
if err := gocommon.MakeDecoder(r).Decode(&data); err != nil {
|
||||
logger.Println("JoinParty failed. DecodeGob returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
character := data.Character
|
||||
gid := data.Gid
|
||||
mid := data.Mid
|
||||
|
||||
if gid.IsZero() || mid.IsZero() {
|
||||
logger.Println("JoinParty failed. mid should be exist")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
gd, err := gp.find(gid)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if gd == nil {
|
||||
// 그룹이 없다. 실패
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 내 정보 업데이트할 때에도 사용됨
|
||||
if data.First {
|
||||
if memdoc, err := gd.addMember(mid, character); err == nil {
|
||||
// 기존 유저에게 새 유저 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: map[string]any{
|
||||
gd.tid(mid): memdoc,
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
|
||||
gp.enterRoom(gid, mid)
|
||||
|
||||
// 최초 입장이라면 새 멤버에 그룹 전체를 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: mid.Hex(),
|
||||
Body: gd.loadFull(),
|
||||
Tag: []string{"GroupDocFull"},
|
||||
})
|
||||
} else if err != nil {
|
||||
logger.Error("JoinParty failed :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
path := "$._members." + gd.tid(mid) + "._body"
|
||||
if _, err := gd.rh.JSONSet(gd.strid(), path, character, gocommon.RedisonSetOptionXX); err != nil {
|
||||
logger.Error("JoinParty failed :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 기존 유저에게 캐릭터 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: map[string]any{
|
||||
gd.tid(mid): bson.M{
|
||||
"_body": character,
|
||||
},
|
||||
},
|
||||
Tag: []string{"MemberDocFragment"},
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// InviteToParty : 초대
|
||||
// - type : 초대 타입 (required)
|
||||
// - from : 초대하는 자 (required)
|
||||
// - to : 초대받는 자 (required)
|
||||
// - timeout : 초대 유지시간(optional. 없으면 config 기본 값)
|
||||
// - (body) : 검색시 노출되는 document
|
||||
func (gp *groupParty) InviteToParty(w http.ResponseWriter, r *http.Request) {
|
||||
var doc struct {
|
||||
Gid primitive.ObjectID
|
||||
Mid primitive.ObjectID
|
||||
Target primitive.ObjectID
|
||||
Inviter bson.M
|
||||
Invitee bson.M
|
||||
}
|
||||
|
||||
if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil {
|
||||
logger.Println("InviteToParty failed. DecodeGob returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
targetid := doc.Target
|
||||
gid := doc.Gid
|
||||
mid := doc.Mid
|
||||
|
||||
// targetid에 초대한 mid가 들어있다.
|
||||
success, err := gp.rh.SetNX(context.Background(), "inv."+targetid.Hex(), mid.Hex(), time.Duration(gp.InviteExpire)*time.Second).Result()
|
||||
if err != nil {
|
||||
logger.Println("InviteToParty failed. gp.rh.SetNX() return err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !success {
|
||||
// 이미 초대 중이다.
|
||||
// inviter한테 알려줘야 한다.
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: mid.Hex(),
|
||||
Body: doc.Invitee,
|
||||
Tag: []string{"InvitationFail"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
gd, err := gp.find(gid)
|
||||
if err != nil {
|
||||
logger.Println("InviteToParty failed. gp.find() return err :", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if gd == nil {
|
||||
gid = primitive.NewObjectID()
|
||||
gd, err = gp.createGroup(gid, mid, doc.Inviter)
|
||||
if err != nil {
|
||||
logger.Println("InviteToParty failed. gp.createGroup() return err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// 내가 wshandler room에 입장
|
||||
gp.enterRoom(gid, mid)
|
||||
gp.rh.JSONSet(mid.Hex(), "$.party", bson.M{"id": gid.Hex()})
|
||||
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: mid.Hex(),
|
||||
Body: gd,
|
||||
Tag: []string{"GroupDocFull"},
|
||||
})
|
||||
}
|
||||
|
||||
newdoc, err := gd.addInvite(targetid, doc.Invitee, time.Duration(gp.InviteExpire+1)*time.Second, gp.MaxMember)
|
||||
if err != nil {
|
||||
logger.Println("InviteToParty failed. gp.addInvite() return err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// invitee에게 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: targetid.Hex(),
|
||||
Body: Invitation{
|
||||
GroupID: gid,
|
||||
TicketID: gd.tid(targetid),
|
||||
Inviter: doc.Inviter,
|
||||
ExpireAtUTC: newdoc.InviteExpire,
|
||||
},
|
||||
Tag: []string{"Invitation"},
|
||||
})
|
||||
}
|
||||
|
||||
func (gp *groupParty) AcceptPartyInvitation(w http.ResponseWriter, r *http.Request) {
|
||||
var doc struct {
|
||||
Gid primitive.ObjectID
|
||||
Mid primitive.ObjectID
|
||||
Tid string
|
||||
Character bson.M
|
||||
}
|
||||
if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil {
|
||||
logger.Println("AcceptPartyInvitation failed. DecodeGob returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
gid := doc.Gid
|
||||
mid := doc.Mid
|
||||
member := doc.Character
|
||||
|
||||
cnt, err := gp.rh.Del(context.Background(), "inv."+mid.Hex()).Result()
|
||||
if err != nil {
|
||||
logger.Error("AcceptPartyInvitation failed. gp.rh.Del returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
// 만료됨
|
||||
w.WriteHeader(http.StatusGatewayTimeout)
|
||||
return
|
||||
}
|
||||
|
||||
pids, err := gp.rh.JSONGetString(mid.Hex(), "$.party.id")
|
||||
if err != nil {
|
||||
logger.Error("AcceptPartyInvitation failed. gp.rh.JSONGetString returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pids) > 0 && len(pids[0]) > 0 {
|
||||
// 기존에 이미 파티에 들어가 있다.
|
||||
// 기존 파티에서는 탈퇴
|
||||
oldgid, _ := primitive.ObjectIDFromHex(pids[0])
|
||||
oldgd := &groupDoc{
|
||||
id: oldgid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
|
||||
// gid에는 제거 메시지 보냄
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + oldgd.strid(),
|
||||
Body: bson.M{
|
||||
oldgd.tid(mid): bson.M{},
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
|
||||
gp.leaveRoom(oldgid, mid)
|
||||
}
|
||||
|
||||
gd := &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.enterRoom(gid, mid)
|
||||
|
||||
// 현재 내 파티를 기록
|
||||
gp.rh.JSONSet(mid.Hex(), "$.party", bson.M{"id": gid.Hex()})
|
||||
|
||||
// 새 멤버에 그룹 전체를 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: mid.Hex(),
|
||||
Body: gd.loadFull(),
|
||||
Tag: []string{"GroupDocFull"},
|
||||
})
|
||||
} else {
|
||||
logger.Println("AcceptPartyInvitation failed. group.AcceptPartyInvitation returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (gp *groupParty) QueryPartyMemberState(w http.ResponseWriter, r *http.Request) {
|
||||
var mid primitive.ObjectID
|
||||
if err := gocommon.MakeDecoder(r).Decode(&mid); err != nil {
|
||||
logger.Println("DenyPartyInvitation failed. DecodeGob returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if cnt, _ := gp.rh.Exists(gp.rh.Context(), mid.Hex()).Result(); cnt == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
states, _ := gp.rh.JSONGetString(mid.Hex(), "$.party.state")
|
||||
if len(states) > 0 && len(states[0]) > 0 {
|
||||
gocommon.MakeEncoder(w, r).Encode(states[0])
|
||||
} else {
|
||||
gocommon.MakeEncoder(w, r).Encode("connected")
|
||||
}
|
||||
}
|
||||
|
||||
func (gp *groupParty) updateMemberDocument(gid groupID, mid accountID, doc bson.M) error {
|
||||
gd := &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.JSONSet(mid.Hex(), "$.party.state", newstate)
|
||||
}
|
||||
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: map[string]any{
|
||||
gd.tid(mid): doc,
|
||||
},
|
||||
Tag: []string{"MemberDocFragment"},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gp *groupParty) updatePartyDocument(gid groupID, frag bson.M) error {
|
||||
gd := 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) {
|
||||
var data struct {
|
||||
Gid primitive.ObjectID
|
||||
Doc bson.M
|
||||
}
|
||||
|
||||
if err := gocommon.MakeDecoder(r).Decode(&data); err != nil {
|
||||
logger.Println("UpdatePartyDocument failed. DecodeGob returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
gid := data.Gid
|
||||
frag := data.Doc
|
||||
|
||||
if err := gp.updatePartyDocument(gid, frag); err != nil {
|
||||
logger.Error("UpdatePartyDocument failed. group.UpdatePartyDocument returns err :", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (gp *groupParty) QueryPartyMembers(w http.ResponseWriter, r *http.Request) {
|
||||
var gid primitive.ObjectID
|
||||
if err := gocommon.MakeDecoder(r).Decode(&gid); err != nil {
|
||||
logger.Println("QueryPartyMembers failed. DecodeGob returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
gd := 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 := gocommon.MakeEncoder(w, r).Encode(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) ClientDisconnected(ctx wshandler.ApiCallContext) {
|
||||
gids, _ := gp.rh.JSONGetString(ctx.CallBy.Accid.Hex(), "$.party.id")
|
||||
|
||||
if len(gids) > 0 && len(gids[0]) > 0 {
|
||||
// mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐
|
||||
gidstr := gids[0]
|
||||
gid, _ := primitive.ObjectIDFromHex(gidstr)
|
||||
|
||||
// 나를 먼저 룸에서 빼야 나한테 메시지가 안감
|
||||
gp.leaveRoom(gid, ctx.CallBy.Accid)
|
||||
|
||||
// gid에는 제거 메시지 보냄
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gidstr,
|
||||
Body: bson.M{
|
||||
makeTid(gid, ctx.CallBy.Accid): bson.M{},
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (gp *groupParty) UpdatePartyMemberDocumentDirect(ctx wshandler.ApiCallContext) {
|
||||
gidobj, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string))
|
||||
doc := ctx.Arguments[1].(map[string]any)
|
||||
|
||||
gp.updateMemberDocument(gidobj, ctx.CallBy.Accid, doc)
|
||||
}
|
||||
|
||||
func (gp *groupParty) UpdatePartyDocumentDirect(ctx wshandler.ApiCallContext) {
|
||||
// 파티 오너만 가능
|
||||
gidobj, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string))
|
||||
doc := ctx.Arguments[1].(map[string]any)
|
||||
|
||||
gd := groupDoc{
|
||||
id: gidobj,
|
||||
rh: gp.rh,
|
||||
}
|
||||
|
||||
incharge, err := gp.rh.JSONGet(gd.strid(), "$._incharge")
|
||||
if err != nil {
|
||||
logger.Println("UpdatePartyDocumentDirect failed. gp.rh.JSONGet returns err :", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(incharge.(string), gd.tid(ctx.CallBy.Accid)) {
|
||||
// incharge가 아니네?
|
||||
logger.Println("UpdatePartyDocumentDirect failed. caller is not incharge")
|
||||
return
|
||||
}
|
||||
|
||||
gp.updatePartyDocument(gidobj, doc)
|
||||
}
|
||||
|
||||
func (gp *groupParty) LeaveParty(ctx wshandler.ApiCallContext) {
|
||||
gids, _ := gp.rh.JSONGetString(ctx.CallBy.Accid.Hex(), "$.party.id")
|
||||
|
||||
if len(gids) == 0 || len(gids[0]) == 0 {
|
||||
return
|
||||
}
|
||||
// mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐
|
||||
gidstr := gids[0]
|
||||
gid, _ := primitive.ObjectIDFromHex(gidstr)
|
||||
mid := ctx.CallBy.Accid
|
||||
tid := ctx.Arguments[0].(string)
|
||||
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
|
||||
var err error
|
||||
if len(tid) > 0 {
|
||||
if tid != gd.tid(mid) {
|
||||
// mid가 incharge여야 한다. 그래야 tid를 쫓아낼 수 있음
|
||||
incharge, err := gp.rh.JSONGet(gd.strid(), "$._incharge")
|
||||
if err != nil {
|
||||
logger.Println("LeaveParty failed. gp.rh.JSONGet returns err :", err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(incharge.(string), gd.tid(mid)) {
|
||||
// incharge가 아니네?
|
||||
logger.Println("LeaveParty failed. mid is not incharge")
|
||||
return
|
||||
}
|
||||
mid = midFromTid(gd.id, tid)
|
||||
}
|
||||
err = gd.removeMemberByTid(tid)
|
||||
} else {
|
||||
err = gd.removeMember(mid)
|
||||
// 내가 나갔다
|
||||
gp.rh.JSONDel(mid.Hex(), "$.party.id")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Println("LeaveParty failed. gd.removeMember returns err :", err)
|
||||
return
|
||||
}
|
||||
|
||||
// mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: mid.Hex(),
|
||||
Body: bson.M{"gid": gid},
|
||||
Tag: []string{"GroupDocFull", gid.Hex()},
|
||||
})
|
||||
|
||||
// gid에는 제거 메시지 보냄
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gd.strid(),
|
||||
Body: bson.M{
|
||||
tid: bson.M{},
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
|
||||
gp.leaveRoom(gid, mid)
|
||||
}
|
||||
|
||||
func (gp *groupParty) DenyPartyInvitation(ctx wshandler.ApiCallContext) {
|
||||
gid, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string))
|
||||
mid := ctx.CallBy.Accid
|
||||
|
||||
gp.rh.Del(context.Background(), "inv."+mid.Hex()).Result()
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
gd.removeMember(mid)
|
||||
}
|
||||
@ -1,383 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"repositories.action2quare.com/ayo/gocommon"
|
||||
"repositories.action2quare.com/ayo/gocommon/logger"
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
)
|
||||
|
||||
const (
|
||||
invitation_collection_name = gocommon.CollectionName("invitation")
|
||||
)
|
||||
|
||||
var invitation_sent_tag = []string{"social.InvitationsSent"}
|
||||
var invitation_received_tag = []string{"social.InvitationsReceived"}
|
||||
var friends_tag = []string{"social.Friends"}
|
||||
|
||||
type invitation struct {
|
||||
mongoClient gocommon.MongoClient
|
||||
redison *gocommon.RedisonHandler
|
||||
wsh *wshandler.WebsocketHandler
|
||||
f *friends
|
||||
}
|
||||
|
||||
type invitationDoc struct {
|
||||
Id primitive.ObjectID `bson:"_id,omitempty" json:"_id"`
|
||||
From primitive.ObjectID `bson:"from,omitempty" json:"-"`
|
||||
To primitive.ObjectID `bson:"to,omitempty" json:"-"`
|
||||
FromAlias string `bson:"falias,omitempty" json:"from"`
|
||||
ToAlias string `bson:"talias,omitempty" json:"to"`
|
||||
Timestamp int64 `bson:"ts" json:"ts"`
|
||||
Deleted bool `bson:"deleted,omitempty" json:"deleted,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]invitationDoc{})
|
||||
}
|
||||
|
||||
func makeInvitation(ctx context.Context, s *Social, f *friends) (*invitation, error) {
|
||||
if err := s.mongoClient.MakeUniqueIndices(invitation_collection_name, map[string]bson.D{
|
||||
"fromto": {{Key: "from", Value: 1}, {Key: "to", Value: 1}},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 내가 받은거
|
||||
if err := s.mongoClient.MakeIndices(invitation_collection_name, map[string]bson.D{
|
||||
"received": {{Key: "to", Value: 1}, {Key: "ts", Value: -1}},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &invitation{
|
||||
mongoClient: s.mongoClient,
|
||||
redison: s.redison,
|
||||
wsh: s.wsh,
|
||||
f: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (iv *invitation) QueryReceivedInvitations(ctx wshandler.ApiCallContext) {
|
||||
// 내가 받은 초대 목록
|
||||
queryfrom := int64(ctx.Arguments[0].(float64))
|
||||
|
||||
var receives []*invitationDoc
|
||||
|
||||
err := iv.mongoClient.FindAllAs(invitation_collection_name, bson.M{
|
||||
"to": ctx.CallBy.Accid,
|
||||
"ts": bson.M{"$gt": queryfrom},
|
||||
}, &receives)
|
||||
if err != nil {
|
||||
logger.Println("QueryReceivedInvitations failed. FindAllAs err :", err)
|
||||
}
|
||||
|
||||
if len(receives) > 0 {
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: ctx.CallBy.Accid.Hex(),
|
||||
Body: receives,
|
||||
Tag: invitation_received_tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (iv *invitation) QuerySentInvitations(ctx wshandler.ApiCallContext) {
|
||||
// 내가 보낸 초대 목록
|
||||
queryfrom := int64(ctx.Arguments[0].(float64))
|
||||
|
||||
var receives []*invitationDoc
|
||||
|
||||
err := iv.mongoClient.FindAllAs(invitation_collection_name, bson.M{
|
||||
"from": ctx.CallBy.Accid,
|
||||
"ts": bson.M{"$gt": queryfrom},
|
||||
"falias": bson.M{"$exists": true},
|
||||
}, &receives)
|
||||
if err != nil {
|
||||
logger.Println("QueryReceivedInvitations failed. FindAllAs err :", err)
|
||||
}
|
||||
|
||||
if len(receives) > 0 {
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: ctx.CallBy.Accid.Hex(),
|
||||
Body: receives,
|
||||
Tag: invitation_sent_tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (iv *invitation) CancelInvitation(ctx wshandler.ApiCallContext) {
|
||||
// ctx.CallBy.Accid
|
||||
id, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string))
|
||||
|
||||
var ivdoc invitationDoc
|
||||
if err := iv.mongoClient.FindOneAs(invitation_collection_name, bson.M{
|
||||
"_id": id,
|
||||
}, &ivdoc); err != nil {
|
||||
logger.Println("CancelInvitation failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ivdoc.From != ctx.CallBy.Accid {
|
||||
return
|
||||
}
|
||||
|
||||
ivdoc.Deleted = true
|
||||
if _, _, err := iv.mongoClient.Update(invitation_collection_name, bson.M{
|
||||
"_id": id,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
"falias": "",
|
||||
"deleted": true,
|
||||
"ts": time.Now().UTC().Unix(),
|
||||
},
|
||||
}); err != nil {
|
||||
logger.Println("CancelInvitation failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: ivdoc.To.Hex(),
|
||||
Body: []invitationDoc{ivdoc},
|
||||
Tag: invitation_received_tag,
|
||||
})
|
||||
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: ivdoc.From.Hex(),
|
||||
Body: []invitationDoc{ivdoc},
|
||||
Tag: invitation_sent_tag,
|
||||
})
|
||||
}
|
||||
|
||||
func (iv *invitation) AcceptInvitation(ctx wshandler.ApiCallContext) {
|
||||
invId, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string))
|
||||
|
||||
var ivdoc invitationDoc
|
||||
if err := iv.mongoClient.FindOneAs(invitation_collection_name, bson.M{
|
||||
"_id": invId,
|
||||
}, &ivdoc); err != nil {
|
||||
logger.Println("AcceptInvitation failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ivdoc.Id != invId {
|
||||
// 초대가 없다
|
||||
return
|
||||
}
|
||||
|
||||
if ivdoc.To != ctx.CallBy.Accid {
|
||||
// 내가 받은 초대가 아니네?
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Unix()
|
||||
f1 := friendDoc{
|
||||
From: ivdoc.To, // 수락한 나
|
||||
To: ivdoc.From, // 상대방
|
||||
ToAlias: ivdoc.FromAlias,
|
||||
Timestamp: now,
|
||||
}
|
||||
f2 := friendDoc{
|
||||
From: ivdoc.From, // 상대방
|
||||
To: ivdoc.To, // 나
|
||||
ToAlias: ivdoc.ToAlias,
|
||||
Timestamp: now,
|
||||
}
|
||||
|
||||
// 나한테 상대방을 친구로 만들고
|
||||
if err := iv.f.addFriend(&f1); err == nil {
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: f1.From.Hex(),
|
||||
Body: []friendDoc{f1},
|
||||
Tag: friends_tag,
|
||||
})
|
||||
} else {
|
||||
logger.Println("AcceptInvitation failed. addFriend(f1) err :", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 상대방한테 나를 친구로 만듬
|
||||
if err := iv.f.addFriend(&f2); err == nil {
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: f2.From.Hex(),
|
||||
Body: []friendDoc{f2},
|
||||
Tag: friends_tag,
|
||||
})
|
||||
} else {
|
||||
logger.Println("AcceptInvitation failed. addFriend(f2) err :", err)
|
||||
return
|
||||
}
|
||||
|
||||
iv.mongoClient.Update(invitation_collection_name, bson.M{
|
||||
"_id": invId,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
"deleted": true,
|
||||
"ts": now,
|
||||
},
|
||||
}, options.Update().SetUpsert(false))
|
||||
}
|
||||
|
||||
func (iv *invitation) DenyInvitation(ctx wshandler.ApiCallContext) {
|
||||
invId, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string))
|
||||
|
||||
var ivdoc invitationDoc
|
||||
if err := iv.mongoClient.FindOneAs(invitation_collection_name, bson.M{
|
||||
"_id": invId,
|
||||
}, &ivdoc); err != nil {
|
||||
logger.Println("AcceptInvitation failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ivdoc.Id != invId {
|
||||
// 초대가 없다
|
||||
return
|
||||
}
|
||||
|
||||
if ivdoc.To != ctx.CallBy.Accid {
|
||||
// 내가 받은 초대가 아니네?
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Unix()
|
||||
ivdoc.Timestamp = now
|
||||
ivdoc.Deleted = true
|
||||
if _, _, err := iv.mongoClient.Update(invitation_collection_name, bson.M{
|
||||
"_id": invId,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
"deleted": true,
|
||||
"ts": now,
|
||||
},
|
||||
}, options.Update().SetUpsert(false)); err != nil {
|
||||
logger.Println("DenyInvitation failed. addFriend(f2) err :", err)
|
||||
return
|
||||
}
|
||||
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: ivdoc.To.Hex(),
|
||||
Body: []invitationDoc{ivdoc},
|
||||
Tag: invitation_received_tag,
|
||||
})
|
||||
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: ivdoc.From.Hex(),
|
||||
Body: []invitationDoc{ivdoc},
|
||||
Tag: invitation_sent_tag,
|
||||
})
|
||||
}
|
||||
|
||||
func (iv *invitation) Trim(ctx wshandler.ApiCallContext) {
|
||||
stringsTobjs := func(in []any) (out []primitive.ObjectID) {
|
||||
for _, i := range in {
|
||||
p, _ := primitive.ObjectIDFromHex(i.(string))
|
||||
out = append(out, p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ids := stringsTobjs(ctx.Arguments[0].([]any))
|
||||
ids = append(ids, stringsTobjs(ctx.Arguments[1].([]any))...)
|
||||
if len(ids) > 0 {
|
||||
if len(ids) == 1 {
|
||||
iv.mongoClient.Delete(invitation_collection_name, bson.M{"_id": ids[0], "deleted": true})
|
||||
} else {
|
||||
iv.mongoClient.DeleteMany(invitation_collection_name, bson.D{
|
||||
{Key: "_id", Value: bson.M{"$in": ids}},
|
||||
{Key: "deleted", Value: true},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (iv *invitation) InviteAsFriend(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. mongodb에 추가
|
||||
// 1-1. block이 되어있다면(==이미 도큐먼트가 있다면) 마치 성공인 것처럼 아무것도 안하고 끝
|
||||
// 2. mongodb에 추가가 성공하면 publish
|
||||
var ivdoc invitationDoc
|
||||
|
||||
if err := gocommon.MakeDecoder(r).Decode(&ivdoc); err != nil {
|
||||
logger.Println("IniviteAsFriend failed:", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ivdoc.Timestamp = time.Now().UTC().Unix()
|
||||
_, newid, err := iv.mongoClient.Update(invitation_collection_name, bson.M{
|
||||
"from": ivdoc.From,
|
||||
"to": ivdoc.To,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
"ts": ivdoc.Timestamp,
|
||||
"falias": ivdoc.FromAlias,
|
||||
"talias": ivdoc.ToAlias,
|
||||
},
|
||||
}, options.Update().SetUpsert(true))
|
||||
if err != nil {
|
||||
logger.Println("IniviteAsFriend failed:", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if newid != nil {
|
||||
ivdoc.Id = newid.(primitive.ObjectID)
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: ivdoc.To.Hex(),
|
||||
Body: []invitationDoc{ivdoc},
|
||||
Tag: invitation_received_tag,
|
||||
})
|
||||
} else {
|
||||
found, _ := iv.mongoClient.FindOne(invitation_collection_name, bson.M{
|
||||
"from": ivdoc.From,
|
||||
"to": ivdoc.To,
|
||||
}, options.FindOne().SetProjection(bson.M{"_id": 1}))
|
||||
|
||||
ivdoc.Id = found["_id"].(primitive.ObjectID)
|
||||
}
|
||||
|
||||
if !ivdoc.Id.IsZero() {
|
||||
iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: ivdoc.From.Hex(),
|
||||
Body: []invitationDoc{ivdoc},
|
||||
Tag: invitation_sent_tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (iv *invitation) Block(w http.ResponseWriter, r *http.Request) {
|
||||
// 초대가 있으면
|
||||
// var bi struct {
|
||||
// From primitive.ObjectID
|
||||
// To primitive.ObjectID
|
||||
// FromAlias string
|
||||
// }
|
||||
// if err := gocommon.MakeDecoder(r).Decode(&bi); err != nil {
|
||||
// logger.Println("invitation.Block failed :", err)
|
||||
// w.WriteHeader(http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// now := time.Now().UTC().Unix()
|
||||
// // From이 To를 block했으므로 To가 From을 초대하는 것을 방지하려면 둘을 뒤집어서 문서를 만들어 놔야 함
|
||||
// // 이미 존재하는 초대일 수도 있다.
|
||||
// _, _, err := iv.mongoClient.Update(invitation_collection_name, bson.M{
|
||||
// "from": bi.To,
|
||||
// "to": bi.From,
|
||||
// }, bson.M{
|
||||
// "$set": invitationDoc{
|
||||
// ToAlias: bi.FromAlias,
|
||||
// Timestamp: now,
|
||||
// },
|
||||
// }, options.Update().SetUpsert(true))
|
||||
// if err != nil {
|
||||
// logger.Println("Block failed:", err)
|
||||
// w.WriteHeader(http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
}
|
||||
134
core/social.go
134
core/social.go
@ -1,134 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"repositories.action2quare.com/ayo/gocommon"
|
||||
"repositories.action2quare.com/ayo/gocommon/flagx"
|
||||
"repositories.action2quare.com/ayo/gocommon/logger"
|
||||
"repositories.action2quare.com/ayo/gocommon/session"
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
)
|
||||
|
||||
var devflag = flagx.Bool("dev", false, "")
|
||||
|
||||
type SocialConfig struct {
|
||||
session.SessionConfig `json:",inline"`
|
||||
|
||||
MaingateApiToken string `json:"maingate_api_token"`
|
||||
RedisURL string `json:"social_redis_url"`
|
||||
MongoURL string `json:"social_storage_url"`
|
||||
}
|
||||
|
||||
var config SocialConfig
|
||||
|
||||
type Social struct {
|
||||
wsh *wshandler.WebsocketHandler
|
||||
mongoClient gocommon.MongoClient
|
||||
redison *gocommon.RedisonHandler
|
||||
httpApiBorker gocommon.HttpApiBroker
|
||||
}
|
||||
|
||||
// New :
|
||||
func New(ctx context.Context, wsh *wshandler.WebsocketHandler, inconfig *SocialConfig) (*Social, error) {
|
||||
if inconfig == nil {
|
||||
var loaded SocialConfig
|
||||
if err := gocommon.LoadConfig(&loaded); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inconfig = &loaded
|
||||
}
|
||||
|
||||
config = *inconfig
|
||||
opt, err := redis.ParseURL(config.RedisURL)
|
||||
if err != nil {
|
||||
return nil, logger.ErrorWithCallStack(err)
|
||||
}
|
||||
|
||||
mc, err := gocommon.NewMongoClient(ctx, config.MongoURL)
|
||||
if err != nil {
|
||||
return nil, logger.ErrorWithCallStack(err)
|
||||
}
|
||||
|
||||
so := &Social{
|
||||
wsh: wsh,
|
||||
redison: gocommon.NewRedisonHandler(ctx, redis.NewClient(opt)),
|
||||
mongoClient: mc,
|
||||
}
|
||||
|
||||
if err := so.prepare(ctx); err != nil {
|
||||
logger.Println("social prepare() failed :", err)
|
||||
return nil, logger.ErrorWithCallStack(err)
|
||||
}
|
||||
|
||||
return so, nil
|
||||
}
|
||||
|
||||
func (so *Social) Cleanup() {
|
||||
so.mongoClient.Close()
|
||||
}
|
||||
|
||||
func (so *Social) prepare(ctx context.Context) error {
|
||||
redisClient, err := gocommon.NewRedisClient(config.RedisURL)
|
||||
if err != nil {
|
||||
return logger.ErrorWithCallStack(err)
|
||||
}
|
||||
|
||||
so.redison = gocommon.NewRedisonHandler(redisClient.Context(), redisClient)
|
||||
|
||||
friends, err := makeFriends(ctx, so)
|
||||
if err != nil {
|
||||
return logger.ErrorWithCallStack(err)
|
||||
}
|
||||
so.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(friends, "social"))
|
||||
so.httpApiBorker.AddHandler(gocommon.MakeHttpApiHandler(friends, "social"))
|
||||
|
||||
invitation, err := makeInvitation(ctx, so, friends)
|
||||
if err != nil {
|
||||
return logger.ErrorWithCallStack(err)
|
||||
}
|
||||
so.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(invitation, "social"))
|
||||
so.httpApiBorker.AddHandler(gocommon.MakeHttpApiHandler(invitation, "social"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (so *Social) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux, prefix string) error {
|
||||
so.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(so, "social"))
|
||||
pattern := gocommon.MakeHttpHandlerPattern(prefix, "api")
|
||||
serveMux.HandleFunc(pattern, so.api)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (so *Social) api(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
s := recover()
|
||||
if s != nil {
|
||||
logger.Error(s)
|
||||
}
|
||||
io.Copy(io.Discard, r.Body)
|
||||
r.Body.Close()
|
||||
}()
|
||||
|
||||
// 서버에서 오는 요청만 처리
|
||||
apitoken := r.Header.Get("MG-X-API-TOKEN")
|
||||
if apitoken != config.MaingateApiToken {
|
||||
// 서버가 보내는 쿼리만 허용
|
||||
logger.Println("MG-X-API-TOKEN is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
funcname := r.URL.Query().Get("call")
|
||||
if len(funcname) == 0 {
|
||||
logger.Println("query param 'call' is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
so.httpApiBorker.Call(funcname, w, r)
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
// warroom project main.go
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestPubSub(t *testing.T) {
|
||||
opt0, _ := redis.ParseURL("redis://192.168.8.94:6380/0")
|
||||
opt1, _ := redis.ParseURL("redis://192.168.8.94:6380/1")
|
||||
|
||||
rc0 := redis.NewClient(opt0)
|
||||
rc1 := redis.NewClient(opt1)
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
rc1.Publish(context.Background(), "__testchan", "real???")
|
||||
fmt.Println("published")
|
||||
}()
|
||||
|
||||
pubsub := rc0.Subscribe(context.Background(), "__testchan")
|
||||
msg, err := pubsub.ReceiveMessage(context.Background())
|
||||
fmt.Println(msg.Payload, err)
|
||||
}
|
||||
func makeHash(chanName string, index uint32) string {
|
||||
for len(chanName) < 12 {
|
||||
chanName += chanName
|
||||
}
|
||||
left := chanName[:6]
|
||||
right := chanName[len(chanName)-6:]
|
||||
base := []byte(left + right)
|
||||
for i := 0; i < 12; i++ {
|
||||
base[i] += base[12-i-1]
|
||||
}
|
||||
|
||||
bs := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(bs, index)
|
||||
for i, c := range bs {
|
||||
base[i] ^= c
|
||||
}
|
||||
var gid primitive.ObjectID
|
||||
copy(gid[:], base)
|
||||
|
||||
return gid.Hex()
|
||||
}
|
||||
|
||||
func TestNameHash(t *testing.T) {
|
||||
for i := 0; i < 10; i++ {
|
||||
makeHash("Urud", uint32(i))
|
||||
fmt.Printf("Urud:%d - %s\n", i, makeHash("Urud", uint32(i)))
|
||||
makeHash("Sheldon", uint32(i))
|
||||
fmt.Printf("Sheldon:%d - %s\n", i, makeHash("Sheldon", uint32(i)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReJSON(t *testing.T) {
|
||||
|
||||
}
|
||||
193
core/tavern.go
Normal file
193
core/tavern.go
Normal file
@ -0,0 +1,193 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"repositories.action2quare.com/ayo/gocommon"
|
||||
"repositories.action2quare.com/ayo/gocommon/flagx"
|
||||
"repositories.action2quare.com/ayo/gocommon/logger"
|
||||
"repositories.action2quare.com/ayo/gocommon/session"
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
var devflag = flagx.Bool("dev", false, "")
|
||||
|
||||
type TavernConfig struct {
|
||||
session.SessionConfig `json:",inline"`
|
||||
Group map[string]configDocument `json:"tavern_group_types"`
|
||||
MaingateApiToken string `json:"maingate_api_token"`
|
||||
RedisURL string `json:"tavern_redis_url"`
|
||||
macAddr string
|
||||
}
|
||||
|
||||
var config TavernConfig
|
||||
|
||||
type Tavern struct {
|
||||
wsh *wshandler.WebsocketHandler
|
||||
mongoClient gocommon.MongoClient
|
||||
redison *gocommon.RedisonHandler
|
||||
httpApiBorker gocommon.HttpApiHandlerContainer
|
||||
}
|
||||
|
||||
func getMacAddr() (string, error) {
|
||||
ifas, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, ifa := range ifas {
|
||||
a := ifa.HardwareAddr.String()
|
||||
if a != "" {
|
||||
a = strings.ReplaceAll(a, ":", "")
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("no net interface")
|
||||
}
|
||||
|
||||
// New :
|
||||
func New(context context.Context, wsh *wshandler.WebsocketHandler) (*Tavern, error) {
|
||||
if err := gocommon.LoadConfig(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
macaddr, err := getMacAddr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.macAddr = macaddr
|
||||
tv := &Tavern{
|
||||
wsh: wsh,
|
||||
}
|
||||
|
||||
if err = tv.prepare(context); err != nil {
|
||||
logger.Println("tavern prepare() failed :", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tv, nil
|
||||
}
|
||||
|
||||
func (tv *Tavern) Cleanup() {
|
||||
tv.mongoClient.Close()
|
||||
}
|
||||
|
||||
func (tv *Tavern) prepare(ctx context.Context) error {
|
||||
redisClient, err := gocommon.NewRedisClient(config.RedisURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tv.redison = gocommon.NewRedisonHandler(redisClient.Context(), redisClient)
|
||||
tv.wsh.RegisterApiHandler(wshandler.MakeWebsocketApiHandler(tv, "tv"))
|
||||
|
||||
if cfg, ok := config.Group["chat"]; ok {
|
||||
chat := new(groupChat)
|
||||
if err := chat.Initialize(tv, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
tv.httpApiBorker.RegisterApiHandler(gocommon.MakeHttpApiHandler(chat, "chat"))
|
||||
tv.wsh.RegisterApiHandler(wshandler.MakeWebsocketApiHandler(chat, "chat"))
|
||||
}
|
||||
|
||||
if cfg, ok := config.Group["party"]; ok {
|
||||
party := new(groupParty)
|
||||
if err := party.Initialize(tv, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
tv.httpApiBorker.RegisterApiHandler(gocommon.MakeHttpApiHandler(party, "party"))
|
||||
tv.wsh.RegisterApiHandler(wshandler.MakeWebsocketApiHandler(party, "party"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tv *Tavern) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux, prefix string) error {
|
||||
// tv.wsh.RegisterReceiver(tv)
|
||||
pattern := gocommon.MakeHttpHandlerPattern(prefix, "api")
|
||||
serveMux.HandleFunc(pattern, tv.api)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tv *Tavern) EnterChannel(ctx wshandler.ApiCallContext) {
|
||||
tv.wsh.EnterRoom(ctx.Arguments[0].(string), ctx.CallBy.Accid)
|
||||
}
|
||||
|
||||
func (tv *Tavern) LeaveChannel(ctx wshandler.ApiCallContext) {
|
||||
tv.wsh.LeaveRoom(ctx.Arguments[0].(string), ctx.CallBy.Accid)
|
||||
}
|
||||
|
||||
func (tv *Tavern) ClientConnected(ctx wshandler.ApiCallContext) {
|
||||
logger.Println("ClientConnected :", ctx.CallBy.Alias)
|
||||
tv.redison.Del(tv.redison.Context(), ctx.CallBy.Accid.Hex())
|
||||
_, err := tv.redison.JSONSet(ctx.CallBy.Accid.Hex(), "$", bson.M{"_ts": time.Now().UTC().Unix()})
|
||||
if err != nil {
|
||||
logger.Println("OnClientMessageReceived HSet error :", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (tv *Tavern) ClientDisconnected(ctx wshandler.ApiCallContext) {
|
||||
tv.redison.Del(tv.redison.Context(), ctx.CallBy.Accid.Hex()).Result()
|
||||
logger.Println("ClientDisconnected :", ctx.CallBy.Alias)
|
||||
}
|
||||
|
||||
func (tv *Tavern) OnRoomCreated(name string) {
|
||||
cnt, err := tv.redison.IncrBy(tv.redison.Context(), "_ref_"+name, 1).Result()
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
logger.Println("OnRoomCreated JSONSet failed :", err)
|
||||
return
|
||||
}
|
||||
|
||||
if cnt == 1 {
|
||||
tv.redison.JSONSet(name, "$", map[string]any{}, gocommon.RedisonSetOptionNX)
|
||||
}
|
||||
}
|
||||
|
||||
func (tv *Tavern) OnRoomDestroyed(name string) {
|
||||
cnt, err := tv.redison.IncrBy(tv.redison.Context(), "_ref_"+name, -1).Result()
|
||||
if err != nil {
|
||||
logger.Println("OnRoomDestroyed JSONNumIncrBy failed :", err)
|
||||
} else if cnt == 0 {
|
||||
tv.redison.Del(tv.redison.Context(), "_ref_"+name)
|
||||
tv.redison.JSONDel(name, "$")
|
||||
}
|
||||
}
|
||||
|
||||
func (tv *Tavern) api(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
s := recover()
|
||||
if s != nil {
|
||||
logger.Error(s)
|
||||
}
|
||||
io.Copy(io.Discard, r.Body)
|
||||
r.Body.Close()
|
||||
}()
|
||||
|
||||
// 서버에서 오는 요청만 처리
|
||||
apitoken := r.Header.Get("MG-X-API-TOKEN")
|
||||
if apitoken != config.MaingateApiToken {
|
||||
// 서버가 보내는 쿼리만 허용
|
||||
logger.Println("MG-X-API-TOKEN is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
funcname := r.URL.Query().Get("call")
|
||||
if len(funcname) == 0 {
|
||||
logger.Println("query param 'call' is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tv.httpApiBorker.Call(funcname, w, r)
|
||||
}
|
||||
113
core/tavern_test.go
Normal file
113
core/tavern_test.go
Normal file
@ -0,0 +1,113 @@
|
||||
// warroom project main.go
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"repositories.action2quare.com/ayo/gocommon"
|
||||
"repositories.action2quare.com/ayo/gocommon/logger"
|
||||
)
|
||||
|
||||
func TestPubSub(t *testing.T) {
|
||||
opt0, _ := redis.ParseURL("redis://192.168.8.94:6380/0")
|
||||
opt1, _ := redis.ParseURL("redis://192.168.8.94:6380/1")
|
||||
|
||||
rc0 := redis.NewClient(opt0)
|
||||
rc1 := redis.NewClient(opt1)
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
rc1.Publish(context.Background(), "__testchan", "real???")
|
||||
fmt.Println("published")
|
||||
}()
|
||||
|
||||
pubsub := rc0.Subscribe(context.Background(), "__testchan")
|
||||
msg, err := pubsub.ReceiveMessage(context.Background())
|
||||
fmt.Println(msg.Payload, err)
|
||||
}
|
||||
func makeHash(chanName string, index uint32) string {
|
||||
for len(chanName) < 12 {
|
||||
chanName += chanName
|
||||
}
|
||||
left := chanName[:6]
|
||||
right := chanName[len(chanName)-6:]
|
||||
base := []byte(left + right)
|
||||
for i := 0; i < 12; i++ {
|
||||
base[i] += base[12-i-1]
|
||||
}
|
||||
|
||||
bs := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(bs, index)
|
||||
for i, c := range bs {
|
||||
base[i] ^= c
|
||||
}
|
||||
var gid primitive.ObjectID
|
||||
copy(gid[:], base)
|
||||
|
||||
return gid.Hex()
|
||||
}
|
||||
|
||||
func TestNameHash(t *testing.T) {
|
||||
for i := 0; i < 10; i++ {
|
||||
makeHash("Urud", uint32(i))
|
||||
fmt.Printf("Urud:%d - %s\n", i, makeHash("Urud", uint32(i)))
|
||||
makeHash("Sheldon", uint32(i))
|
||||
fmt.Printf("Sheldon:%d - %s\n", i, makeHash("Sheldon", uint32(i)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReJSON(t *testing.T) {
|
||||
rc := redis.NewClient(&redis.Options{Addr: "192.168.8.94:6380"})
|
||||
rh := gocommon.NewRedisonHandler(context.Background(), rc)
|
||||
|
||||
success, err := rc.HSetNX(context.Background(), "setnxtest", "cap", 100).Result()
|
||||
fmt.Println(success, err)
|
||||
success, err = rc.HSetNX(context.Background(), "setnxtest", "cap", 100).Result()
|
||||
fmt.Println(success, err)
|
||||
|
||||
testDoc := map[string]any{
|
||||
"members": map[string]any{
|
||||
"mid2": map[string]any{
|
||||
"key": "val",
|
||||
"exp": 20202020,
|
||||
},
|
||||
"mid1": map[string]any{
|
||||
"key": "val",
|
||||
"exp": 10101010,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gd := groupDoc{
|
||||
id: primitive.NewObjectID(),
|
||||
}
|
||||
|
||||
midin := primitive.NewObjectID()
|
||||
tid := gd.tid(midin)
|
||||
midout := gd.mid(tid)
|
||||
logger.Println(midin, tid, midout)
|
||||
|
||||
logger.Println(rh.JSONSet("jsontest", "$", testDoc))
|
||||
logger.Println(rh.JSONGet("jsontest", "$"))
|
||||
logger.Println(rh.JSONResp("jsontest", "$.members"))
|
||||
logger.Println(rh.JSONGetString("jsontest", "$.members..key"))
|
||||
logger.Println(rh.JSONGetInt64("jsontest", "$.members..exp"))
|
||||
logger.Println(rh.JSONObjKeys("jsontest", "$.members"))
|
||||
|
||||
err = rh.JSONMSet("jsontest", map[string]any{
|
||||
"$.members.mid1.key": "newval",
|
||||
"$.members.mid2.key": "newval",
|
||||
})
|
||||
logger.Println(err)
|
||||
|
||||
logger.Println(rh.JSONGet("jsontest", "$"))
|
||||
logger.Println(rh.JSONMDel("jsontest", []string{"$.members.mid1", "$.members.mid2"}))
|
||||
logger.Println(rh.JSONGet("jsontest", "$"))
|
||||
logger.Println(rh.JSONObjLen("jsontest", "$.members"))
|
||||
}
|
||||
Reference in New Issue
Block a user