11 Commits

Author SHA1 Message Date
8e1b232d57 InMemory 그룹을 redis로 변경 2023-07-19 09:37:02 +09:00
01da5bb3a4 body를 marshaling하고 클라이언트에서 flatten함 2023-07-18 01:31:39 +09:00
ba61a11659 gob 등록 2023-07-17 17:47:07 +09:00
67cca13326 모듈 업데이트 2023-07-16 18:41:24 +09:00
272c696c59 json value 다시 되돌림 2023-07-16 17:29:21 +09:00
aa568ec3fa SetOption 타입 변경 2023-07-16 17:26:19 +09:00
b9c4d8b21b objvalue marshalling 수정 2023-07-16 17:15:08 +09:00
99834c1461 objlen 수정 2023-07-16 17:01:06 +09:00
592112219e gocommon 업데이트 2023-07-16 16:38:05 +09:00
62485b6d54 redis json 마이그레이션 완료 2023-07-16 15:36:20 +09:00
d36dd13bb7 redis stack 사용 2023-07-16 02:51:41 +09:00
15 changed files with 1437 additions and 1838 deletions

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
.vscode/
__debug_bin.exe
*.log
config.json
tavern.exe

View File

@ -1,49 +1 @@
{
"region_storage": {
"default": {
"mongo": "mongodb://192.168.8.94:27017/?replicaSet=repl01&retrywrites=false",
"redis": {
"cache": "redis://192.168.8.94:6380/0",
"session": "redis://192.168.8.94:6380/1",
"tx": "redis://192.168.8.94:6380/2",
"tavern": "redis://192.168.8.94:6380/3",
"wshandler": "redis://192.168.8.94:6380/4"
}
}
},
"session_storage": "redis://192.168.8.94:6380/3",
"session_ttl": 3600,
"maingate_session_storage": "redis://192.168.8.94:6380/1",
"maingate_session_ttl" : 3600,
"maingate_api_token": "63d08aa34f0162622c11284b",
"tavern_redis_url": "redis://192.168.8.94:6380/4",
"tavern_service_url": "http://localhost/tavern",
"tavern_group_types": {
"party": {
"max_member": 3,
"invite_ttl": 30
},
"chat" : {
"default_capacity" : 1000,
"channels" : {
"bazzar-1" : {
"name" : "FText(bazzar-1)"
},
"bazzar-2" : {
"name" : "FText(bazzar-2)"
},
"bazzar-3" : {
"name" : "FText(bazzar-3)"
},
"bazzar-4" : {
"name" : "FText(bazzar-4)"
},
"bazzar-5" : {
"name" : "FText(bazzar-5)"
}
}
}
}
}
{}

View File

@ -5,42 +5,68 @@
"redis": {
"cache": "redis://192.168.8.94:6380/0",
"session": "redis://192.168.8.94:6380/1",
"tx": "redis://192.168.8.94:6380/2",
"tavern": "redis://192.168.8.94:6380/3",
"wshandler": "redis://192.168.8.94:6380/4"
"ranking": "redis://192.168.8.94:6380/2",
"wshandler": "redis://192.168.8.94:6380/3",
"tavern": "redis://192.168.8.94:6380/4"
}
},
"dev": {
"mongo": "mongodb://192.168.8.94:27017/?replicaSet=repl01&retrywrites=false",
"redis": {
"cache": "redis://192.168.8.94:6380/4",
"session": "redis://192.168.8.94:6380/5",
"ranking": "redis://192.168.8.94:6380/6",
"wshandler": "redis://192.168.8.94:6380/7",
"tavern": "redis://192.168.8.94:6380/8"
}
}
},
"session_storage": "redis://192.168.8.94:6380/5",
"session_ttl": 3600,
"maingate_mongodb_url": "mongodb://192.168.8.94:27017/?replicaSet=repl01&retrywrites=false",
"maingate_service_url": "http://localhost/maingate",
"maingate_api_token": "63d08aa34f0162622c11284b",
"tavern_redis_url": "redis://192.168.8.94:6380/7",
"tavern_service_url": "http://localhost/tavern",
"tavern_group_types": {
"party": {
"max_member": 3,
"invite_ttl": 30
"subjugate": {
"text_search_field": [
"name"
],
"unique_index": [
"name,_id",
"_id,members",
"name,hidden"
],
"search_index": [
"rules"
],
"member_index": [
"_gid,candidate,luts",
"_gid,luts",
"_gid,expiring"
],
"invite_ttl": 30,
"candidate_ttl": 3600,
"invitee_exlusive": true,
"invitee_is_member": true,
"max_member": 4
},
"chat" : {
"default_capacity" : 1000,
"channels" : {
"bazzar-1" : {
"name" : "FText(bazzar-1)"
},
"bazzar-2" : {
"name" : "FText(bazzar-2)"
},
"bazzar-3" : {
"name" : "FText(bazzar-3)"
},
"bazzar-4" : {
"name" : "FText(bazzar-4)"
},
"bazzar-5" : {
"name" : "FText(bazzar-5)"
}
"lobby": {
"max_member": 3,
"invitee_exlusive": true,
"invitee_is_member": true,
"transient": true,
"invite_ttl": 30
}
},
"services": {
"kingdom": {
"개발중": {
"url": "http://localhost/warehouse/dev",
"development": true
},
"개인서버": {
"url": "http://localhost/warehouse/private",
"development": false
}
}
}

526
core/apiimpl.go Normal file
View File

@ -0,0 +1,526 @@
package core
import (
"net/http"
common "repositories.action2quare.com/ayo/gocommon"
"repositories.action2quare.com/ayo/gocommon/logger"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// CreateGroup : 그룹 생성
// - 그룹 : 멤버와 권한을 관리할 수 있다. 그룹 타입에 따라 디비에 저장되거나 메모리에만 존재한다.
// - 생성 요청이 오면 파티를 만든다. 파티을 만들 수 있는지 여부는 서비스에서 결정할 것이고, 이 요청을 호출했다는 것은 서비스가 결정한 그룹 생성 조건을 다 통과했다는 의미이다.
// - parameter :
// - type : 그룹 종류. 그룹 종류에 따라 인덱스와 쿼리 가능 field가 다르다.
func (sub *subTavern) CreateGroup(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
grouptype := sub.groups[typename]
if grouptype == nil {
logger.Println("CreateGroup failed. group type is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
doc := bson.M{}
if err := readBsonDoc(r.Body, &doc); err != nil {
logger.Error("CreateGroup failed. readBsonDoc returns err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
inserted, err := grouptype.Create(r.Form, doc)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.Write(inserted[:])
}
// JoinGroup : 그룹에 참가
// - type : 그룹 타입
// - 그룹 타입에 맞는 키(주로 _id)
// - member_id : 참가 멤버의 아이디
// - body : 멤버의 속성 bson document
func (sub *subTavern) JoinGroup(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("JoinGroup failed. group type is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
doc := bson.M{}
if err := readBsonDoc(r.Body, &doc); err != nil {
logger.Error("JoinGroup failed. readBsonDoc returns err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
gidobj, ok := common.ReadObjectIDFormValue(r.Form, "gid")
if !ok {
logger.Println("JoinGroup failed. gid is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
midobj, midok := common.ReadObjectIDFormValue(r.Form, "mid")
tidobj, tidok := common.ReadObjectIDFormValue(r.Form, "tid")
if !midok && !tidok {
// 둘다 없네?
logger.Println("JoinGroup failed. tid or mid should be exist")
w.WriteHeader(http.StatusBadRequest)
return
}
var err error
if candidate, ok := common.ReadBoolFormValue(r.Form, "candidate"); ok && candidate {
err = group.Candidate(gidobj, midobj, doc)
} else {
err = group.Join(gidobj, midobj, doc)
}
if err == nil {
writeBsonDoc(w, map[string]string{
"gid": gidobj.Hex(),
"tid": tidobj.Hex(),
})
} else if err == errGroupNotExist {
w.Write([]byte("{}"))
} else if err != nil {
logger.Error("JoinGroup failed :", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
// Invite : 초대
// - type : 초대 타입 (required)
// - from : 초대하는 자 (required)
// - to : 초대받는 자 (required)
// - timeout : 초대 유지시간(optional. 없으면 config 기본 값)
// - (body) : 검색시 노출되는 document
func (sub *subTavern) Invite(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("Invite failed. group type is missing :", r)
w.WriteHeader(http.StatusBadRequest)
return
}
gid, ok := common.ReadObjectIDFormValue(r.Form, "gid")
if !ok {
logger.Println("Invite failed. gid is missing :", r)
w.WriteHeader(http.StatusBadRequest)
return
}
mid, ok := common.ReadObjectIDFormValue(r.Form, "mid")
if !ok {
logger.Println("Invite failed. mid is missing :", r)
w.WriteHeader(http.StatusBadRequest)
return
}
var reqdoc struct {
Inviter bson.M `bson:"inviter"`
Invitee bson.M `bson:"invitee"`
}
if err := readBsonDoc(r.Body, &reqdoc); err != nil {
logger.Println("Invite failed. readBsonDoc returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
result, err := group.Invite(gid, mid, reqdoc.Inviter, reqdoc.Invitee)
if err != nil {
logger.Println("Invite failed. group.Invite returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write([]byte(result))
}
func (sub *subTavern) CancelInvitation(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("CancelInvitation failed. group type is missing :", r)
w.WriteHeader(http.StatusBadRequest)
return
}
tid, ok := common.ReadObjectIDFormValue(r.Form, "tid")
if !ok {
logger.Println("CancelInvitation failed. form value 'tid' is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
gid, ok := common.ReadObjectIDFormValue(r.Form, "gid")
if !ok {
logger.Println("CancelInvitation failed. form value 'gid' is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
if err := group.CancelInvitation(gid, tid); err != nil {
logger.Println("CancelInvitation failed. group.CancelInvitation returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func (sub *subTavern) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("CancelInvitation failed. group type is missing :", r)
w.WriteHeader(http.StatusBadRequest)
return
}
gid, _ := common.ReadObjectIDFormValue(r.Form, "gid")
mid, _ := common.ReadObjectIDFormValue(r.Form, "mid")
var member bson.M
if err := readBsonDoc(r.Body, &member); err != nil {
logger.Error("AcceptInvitation failed. readBsonDoc returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err := group.AcceptInvitation(gid, mid, member)
if err != nil {
logger.Println("AcceptInvitation failed. group.AcceptInvitation returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (sub *subTavern) DenyInvitation(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("DenyInvitation failed. group type is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
gid, _ := common.ReadObjectIDFormValue(r.Form, "gid")
mid, _ := common.ReadObjectIDFormValue(r.Form, "mid")
tid, ok := common.ReadObjectIDFormValue(r.Form, "tid")
if !ok {
logger.Println("DenyInvitation failed. tid is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
err := group.DenyInvitation(gid, mid, tid)
if err != nil {
logger.Error("DenyInvitation failed. group.DenyInvitation returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (sub *subTavern) QueryInvitations(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("QueryInvitations failed. group type is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
mid, ok := common.ReadObjectIDFormValue(r.Form, "mid")
if !ok {
logger.Println("QueryInvitations failed. mid is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
var after primitive.Timestamp
if v, ok := common.ReadStringFormValue(r.Form, "after"); ok && v != "0.0" {
after = common.DotStringToTimestamp(v)
}
result, err := group.QueryInvitations(mid, after)
if err != nil {
logger.Println("QueryInvitations failed. group.QueryInvitations returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if err := writeBsonArr(w, result); err != nil {
logger.Println("QueryInvitations failed. Encode returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (sub *subTavern) SearchGroup(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("SearchGroup failed. group type is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
projection, _ := common.ReadStringFormValue(r.Form, "projection")
var filter bson.M
if err := readBsonDoc(r.Body, &filter); err != nil {
logger.Error("SearchGroup failed. readBsonDoc returns err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
result, err := group.FindAll(filter, projection, primitive.Timestamp{})
if err != nil {
logger.Error("SearchGroup failed. FindAll err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if result == nil {
return
}
if err := writeBsonArr(w, result); err != nil {
logger.Error("bson marshal failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (sub *subTavern) QueryOnlineState(w http.ResponseWriter, r *http.Request) {
mid, ok := common.ReadObjectIDFormValue(r.Form, "mid")
if !ok {
logger.Println("IsOnline failed. mid is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
state := sub.wsh.GetState(sub.region, mid)
w.Write([]byte(state))
}
func (sub *subTavern) IsOnline(w http.ResponseWriter, r *http.Request) {
mid, ok := common.ReadObjectIDFormValue(r.Form, "mid")
if !ok {
logger.Println("IsOnline failed. mid is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
if state := sub.wsh.GetState(sub.region, mid); len(state) > 0 {
w.Write([]byte("true"))
} else {
w.Write([]byte("false"))
}
}
// QueryGroup : 그룹조회
// - type : 그룹 타입
// - 그룹 타입에 맞는 키(주로 _id)
// - projection : select할 필드. ,로 구분
func (sub *subTavern) QueryGroup(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("QueryGroup failed. group type is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
gid, ok := common.ReadObjectIDFormValue(r.Form, "_id")
if !ok {
logger.Println("QueryGroup failed. _id is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
projection, _ := common.ReadStringFormValue(r.Form, "projection")
after, _ := common.ReadStringFormValue(r.Form, "after")
if after != "0.0" {
projection += ",+luts"
}
result, err := group.FindOne(gid, projection)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if result == nil {
return
}
if len(after) > 0 {
if luts, ok := result["luts"].(primitive.Timestamp); ok {
afterts := common.DotStringToTimestamp(after)
if primitive.CompareTimestamp(luts, afterts) < 0 {
return
}
}
}
if err := writeBsonDoc(w, result); err != nil {
logger.Error("bson marshal failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
// LeaveGroup : 그룹에서 나감 or 내보냄
// - type : 그룹 타입
// - 그룹 타입에 맞는 키(주로 _id)
// - member_id : 나갈 멤버의 아이디
func (sub *subTavern) LeaveGroup(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("LeaveGroup failed. group type is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
gid, ok := common.ReadObjectIDFormValue(r.Form, "gid")
if !ok {
logger.Println("LeaveGroup failed. gid is missing :", r.Form)
w.WriteHeader(http.StatusBadRequest)
return
}
mid, midok := common.ReadObjectIDFormValue(r.Form, "mid")
if !midok {
logger.Println("LeaveGroup failed. mid is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
if err := group.Leave(gid, mid); err != nil {
// 둘 중 하나는 있어야지
logger.Println("LeaveGroup failed. group.Leave returns err :", err)
w.WriteHeader(http.StatusBadRequest)
}
}
func (sub *subTavern) UpdateMemberDocument(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("DismissGroup failed. type is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
midobj, ok := common.ReadObjectIDFormValue(r.Form, "mid")
if !ok {
logger.Println("UpdateMemberDocument failed. member_id is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
gidobj, ok := common.ReadObjectIDFormValue(r.Form, "gid")
if !ok {
logger.Println("UpdateMemberDocument failed. _id is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
var updatedoc bson.M
if err := readBsonDoc(r.Body, &updatedoc); err != nil {
logger.Error("UpdateMemberDocument failed. body decoding error :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
if err := group.UpdateMemberDocument(gidobj, midobj, updatedoc); err != nil {
logger.Println("UpdateMemberDocument failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (sub *subTavern) DismissGroup(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("DismissGroup failed. type is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
gid, ok := common.ReadObjectIDFormValue(r.Form, "gid")
if !ok {
logger.Println("DismissGroup failed. gid is missing :")
w.WriteHeader(http.StatusBadRequest)
return
}
if err := group.Dismiss(gid); err != nil {
logger.Error("DismissGroup failed. group.Dismiss returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (sub *subTavern) UpdateGroupDocument(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("UpdateGroupDocument failed. type is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
gid, ok := common.ReadObjectIDFormValue(r.Form, "gid")
if !ok {
logger.Println("UpdateGroupDocument failed. gid is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
var frag bson.M
if err := readBsonDoc(r.Body, &frag); err != nil {
logger.Error("UpdateGroupDocument failed. readBsonDoc err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
if err := group.UpdateGroupDocument(gid, frag); err != nil {
logger.Error("UpdateGroupDocument failed. group.UpdateGroupDocument returns err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
}
func (sub *subTavern) QueryGroupMembers(w http.ResponseWriter, r *http.Request) {
typename, _ := common.ReadStringFormValue(r.Form, "type")
group := sub.groups[typename]
if group == nil {
logger.Println("QueryGroupMembers failed. type is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
gid, ok := common.ReadObjectIDFormValue(r.Form, "gid")
if !ok {
logger.Println("QueryGroupMembers failed. gid is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
members, err := group.QueryGroupMembers(gid)
if err != nil {
logger.Error("QueryGroupMembers failed. group.QueryGroupMembers returns err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
if err := writeBsonDoc(w, members); err != nil {
logger.Error("QueryGroupMembers failed. writeBsonDoc return err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}

1
core/config.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -1,3 +1,42 @@
package core
type configDocument map[string]any
import (
"net/url"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type groupConfig struct {
UniqueIndex []string `json:"unique_index"`
SearchIndex []string `json:"search_index"`
MemberIndex []string `json:"member_index"`
TextSearchFields []string `json:"text_search_field"`
InviteExpire int32 `json:"invite_ttl"` // 그룹이 개인에게 보낸 초대장 만료 기한
CandidateExpire int32 `json:"candidate_ttl"` // 개인이 그룹에게 보낸 신청서 만료 기한
InviteeExlusive bool `json:"invitee_exlusive"`
InviteeIsMember bool `json:"invitee_is_member"`
MaxMember int `json:"max_member"`
Transient bool `json:"transient"`
Name string
}
type group interface {
Create(form url.Values, doc bson.M) (primitive.ObjectID, error)
Candidate(groupID primitive.ObjectID, memberID primitive.ObjectID, doc bson.M) error
Join(groupID primitive.ObjectID, memberID primitive.ObjectID, doc bson.M) error
Invite(groupID primitive.ObjectID, memberID primitive.ObjectID, inviterDoc bson.M, inviteeDoc bson.M) (string, error)
CancelInvitation(groupID primitive.ObjectID, ticketID primitive.ObjectID) error
AcceptInvitation(groupID primitive.ObjectID, mid primitive.ObjectID, member bson.M) error
DenyInvitation(groupID primitive.ObjectID, mid primitive.ObjectID, ticketID primitive.ObjectID) error
QueryInvitations(memberID primitive.ObjectID, after primitive.Timestamp) ([]bson.M, error)
Exist(groupID primitive.ObjectID, filter bson.M) (bool, error)
FindAll(filter bson.M, projection string, after primitive.Timestamp) ([]bson.M, error)
FindOne(groupID primitive.ObjectID, projection string) (bson.M, error)
Leave(groupID primitive.ObjectID, memberID primitive.ObjectID) error
UpdateMemberDocument(groupID primitive.ObjectID, memberID primitive.ObjectID, doc bson.M) error
Dismiss(groupID primitive.ObjectID) error
UpdateGroupDocument(groupID primitive.ObjectID, doc bson.M) error
QueryGroupMembers(groupID primitive.ObjectID) (bson.M, error)
}

View File

@ -1,348 +0,0 @@
package core
import (
"encoding/gob"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-redis/redis/v8"
"github.com/gorilla/websocket"
"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"`
Members map[string]int32 `json:"members"`
emptyJson string
inoutChan chan 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 init() {
gob.Register(map[string]string{})
}
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))
cfg.Members = make(map[string]int32)
_, 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
}
inoutchan := make(chan string, 10)
cfg.inoutChan = inoutchan
go func(chanid string) {
var cur []string
tick := time.After(3 * time.Second)
for {
select {
case <-tick:
tick = time.After(3 * time.Second)
if len(cur) > 0 {
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + chanid,
Body: map[string]any{"inout": cur},
Tag: []string{"ChattingChannelProperties"},
})
cur = nil
}
case m := <-inoutchan:
cur = append(cur, m)
}
}
}(name)
}
return nil
}
func (gc *groupChat) ClientConnected(conn *websocket.Conn, callby *wshandler.Sender) {
}
func (gc *groupChat) ClientDisconnected(msg string, callby *wshandler.Sender) {
if msg == wshandler.ForceShutdownCloseMessage {
gc.rh.Del(gc.rh.Context(), "gc-"+callby.Accid.Hex()).Result()
return
}
chans, _ := gc.rh.HGetAll(gc.rh.Context(), "gc-"+callby.Accid.Hex()).Result()
gc.rh.Del(gc.rh.Context(), "gc-"+callby.Accid.Hex()).Result()
for typename, chanid := range chans {
gc.leaveRoom(chanid, callby.Accid)
if typename == "public" {
cnt, err := gc.rh.JSONDel(chanid, "$.members."+callby.Alias)
if cnt > 0 {
if cfg, ok := gc.chatConfig.Channels[chanid]; ok {
cfg.inoutChan <- "-" + callby.Alias
}
} else if err != nil {
logger.Println("groupchat ClientDisconnected JSONDel err :", err, callby.Alias)
} else {
logger.Println("groupchat ClientDisconnected JSONDel cnt 0 :", callby.Alias)
}
} else {
logger.Println("groupchat ClientDisconnected leave private channel :", chanid, callby.Alias)
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + chanid,
Body: map[string]any{"sender": callby.Alias},
Tag: []string{typename + ".LeavePrivateChannel"},
})
}
}
}
func (gc *groupChat) EnterPublicChannel(ctx wshandler.ApiCallContext) {
gc.LeavePublicChannel(ctx)
chanid := ctx.Arguments[0].(string)
cfg, ok := gc.chatConfig.Channels[chanid]
if !ok {
return
}
atomicsize, _ := gc.rh.JSONObjLen(chanid, "$.members")
if len(atomicsize) == 0 {
return
}
if atomicsize[0] >= cfg.Capacity {
logger.Println("chatting channel is full :", chanid, atomicsize[0], cfg.Capacity)
return
}
cnt, _ := gc.rh.HSet(gc.rh.Context(), "gc-"+ctx.CallBy.Accid.Hex(), "public", chanid).Result()
if cnt == 0 {
logger.Println("HSet cnt 0 :", chanid, ctx.CallBy.Alias)
return
}
ok, err := gc.rh.JSONSet(chanid, "$.members."+ctx.CallBy.Alias, 1)
if err != nil {
gc.rh.HDel(gc.rh.Context(), "gc-"+ctx.CallBy.Accid.Hex(), "public").Result()
logger.Println("JSONSet $.members failed :", err, chanid, ctx.CallBy.Alias)
return
}
if !ok {
gc.rh.HDel(gc.rh.Context(), "gc-"+ctx.CallBy.Accid.Hex(), "public").Result()
logger.Println("JSONSet $.members not ok :", chanid, ctx.CallBy.Alias)
return
}
gc.enterRoom(chanid, ctx.CallBy.Accid)
cfg.inoutChan <- "+" + ctx.CallBy.Alias
members, _ := gc.rh.JSONGetDocuments(chanid, "$.members")
var toarr []string
if len(members) > 0 {
toarr = make([]string, 0, len(members[0]))
for k := range members[0] {
toarr = append(toarr, k)
}
} else {
toarr = []string{}
}
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: ctx.CallBy.Accid.Hex(),
Body: map[string]any{"members": toarr},
Tag: []string{"ChattingChannelProperties"},
})
}
func (gc *groupChat) LeavePublicChannel(ctx wshandler.ApiCallContext) {
oldchan, _ := gc.rh.HGet(gc.rh.Context(), "gc-"+ctx.CallBy.Accid.Hex(), "public").Result()
if len(oldchan) > 0 {
gc.rh.HDel(gc.rh.Context(), "gc-"+ctx.CallBy.Accid.Hex(), "public")
cnt, err := gc.rh.JSONDel(oldchan, "$.members."+ctx.CallBy.Alias)
if cnt > 0 {
gc.leaveRoom(oldchan, ctx.CallBy.Accid)
if cfg, ok := gc.chatConfig.Channels[oldchan]; ok {
cfg.inoutChan <- "-" + ctx.CallBy.Alias
}
} else if err != nil {
logger.Println("groupchat LeavePublicChannel JSONDel err :", err, oldchan, ctx.CallBy.Alias)
} else {
logger.Println("groupchat LeavePublicChannel JSONDel cnt 0 :", oldchan, ctx.CallBy.Alias)
}
}
}
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.HSetNX(gc.rh.Context(), "gc-"+ctx.CallBy.Accid.Hex(), typename, channel).Result()
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,
},
Tag: []string{typename + ".EnterPrivateChannel"},
})
}
func (gc *groupChat) LeavePrivateChannel(ctx wshandler.ApiCallContext) {
typename := ctx.Arguments[0].(string)
chanid := ctx.Arguments[1].(string)
cnt, _ := gc.rh.HDel(gc.rh.Context(), "gc-"+ctx.CallBy.Accid.Hex(), typename).Result()
if cnt > 0 {
gc.leaveRoom(chanid, ctx.CallBy.Accid)
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + chanid,
Body: map[string]any{"sender": ctx.CallBy.Alias},
Tag: []string{typename + ".LeavePrivateChannel"},
})
}
}
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.HGetAll(gc.rh.Context(), "gc-"+accid.Hex()).Result()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if sub == nil {
sub = make(map[string]string)
}
gocommon.MakeEncoder(w, r).Encode(sub)
}
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)
}

View File

@ -1,243 +0,0 @@
package core
import (
"errors"
"net/http"
"repositories.action2quare.com/ayo/gocommon"
"repositories.action2quare.com/ayo/gocommon/logger"
"repositories.action2quare.com/ayo/gocommon/wshandler"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type instantDoc struct {
Members map[string]primitive.M `json:"_members"`
Count int64 `json:"_count"`
Body primitive.M `json:"_body"`
Gid primitive.ObjectID `json:"_gid"`
rh *gocommon.RedisonHandler
idstr string
}
func (gd *instantDoc) strid() string {
if len(gd.idstr) == 0 {
gd.idstr = gd.Gid.Hex()
}
return gd.idstr
}
func (gd *instantDoc) tid(in accountID) string {
return makeTid(gd.Gid, in)
}
var errGroupAlreadyDestroyed = errors.New("instant group is already destroyed")
func (gd *instantDoc) removeMember(mid accountID) error {
counts, _ := gd.rh.JSONNumIncrBy(gd.strid(), "$._count", -1)
if len(counts) == 0 {
// 이미 지워진 인스턴트그룹
return errGroupAlreadyDestroyed
}
if _, err := gd.rh.JSONDel(gd.strid(), "$._members."+gd.tid(mid)); err != nil {
return err
}
gd.Count = counts[0]
return nil
}
type groupInstant struct {
sendUpstreamMessage func(*wshandler.UpstreamMessage)
enterRoom func(groupID, accountID)
leaveRoom func(groupID, accountID)
rh *gocommon.RedisonHandler
}
func (gi *groupInstant) Initialize(tv *Tavern) error {
gi.rh = tv.redison
gi.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) {
tv.wsh.SendUpstreamMessage(msg)
}
gi.enterRoom = func(gid groupID, accid accountID) {
tv.wsh.EnterRoom(gid.Hex(), accid)
}
gi.leaveRoom = func(gid groupID, accid accountID) {
tv.wsh.LeaveRoom(gid.Hex(), accid)
}
return nil
}
func (gi *groupInstant) UpdateInstantDocument(w http.ResponseWriter, r *http.Request) {
var data struct {
Gid primitive.ObjectID
Doc primitive.M
Result string
}
if err := gocommon.MakeDecoder(r).Decode(&data); err != nil {
logger.Println("UpdateInstantDocument failed. Decode returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
gd := partyDoc{
id: data.Gid,
rh: gi.rh,
}
if err := gi.rh.JSONMSetRel(gd.strid(), "$.", data.Doc); err != nil {
logger.Println("UpdateInstantDocument failed. JSONMSetRel returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 업데이트 알림
gi.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gd.strid(),
Body: data.Doc,
Tag: []string{"GroupDocFragment"},
})
if data.Result == "after" {
fulldoc := gd.loadFull()
if fulldoc != nil {
tids := fulldoc["_members"].(map[string]any)
mids := make(map[string]any)
for k, v := range tids {
mid := midFromTid(data.Gid, k)
mids[mid.Hex()] = v
}
fulldoc["_members"] = mids
}
gocommon.MakeEncoder(w, r).Encode(fulldoc)
}
}
func (gi *groupInstant) Build(w http.ResponseWriter, r *http.Request) {
var data struct {
Members []struct {
Mid primitive.ObjectID
Character primitive.M
}
Body primitive.M
}
if err := gocommon.MakeDecoder(r).Decode(&data); err != nil {
logger.Println("JoinParty failed. DecodeGob returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// data.Members가 다 살아있는지 부터 확인
var result struct {
Gid primitive.ObjectID // 실패하면 없음
Success []primitive.ObjectID
}
for _, m := range data.Members {
count, err := gi.rh.Exists(gi.rh.Context(), m.Mid.Hex()).Result()
if err != nil {
logger.Println("instant.Build failed. Exists returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if count != 0 {
result.Success = append(result.Success, m.Mid)
}
}
if len(result.Success) < len(data.Members) {
// 이탈자가 있군
// 여기서 중단
gocommon.MakeEncoder(w, r).Encode(result)
return
}
newid := primitive.NewObjectID()
members := make(map[string]primitive.M)
for _, m := range data.Members {
tid := makeTid(newid, m.Mid)
members[tid] = m.Character
}
gd := &instantDoc{
Members: members,
Body: data.Body,
Count: int64(len(members)),
rh: gi.rh,
Gid: newid,
}
_, err := gi.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX)
if err != nil {
logger.Println("instant.Build failed. JSONSet returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
result.Gid = newid
for _, char := range members {
delete(char, "_id")
}
// 멤버에 그룹 전체를 알림
for _, m := range data.Members {
midstr := m.Mid.Hex()
gi.rh.JSONSet(midstr, "$.instant", bson.M{"id": gd.strid()})
gi.enterRoom(newid, m.Mid)
gi.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: midstr,
Body: gd,
Tag: []string{"GroupDocFull"},
})
}
gocommon.MakeEncoder(w, r).Encode(result)
}
func (gi *groupInstant) makeClientClean(accid primitive.ObjectID) {
gids, _ := gi.rh.JSONGetString(accid.Hex(), "$.instant.id")
if len(gids) > 0 && len(gids[0]) > 0 {
gidstr := gids[0]
gid, _ := primitive.ObjectIDFromHex(gidstr)
gi.rh.JSONDel(accid.Hex(), "$.instant.id")
gi.leaveRoom(gid, accid)
// gid에는 제거 메시지 보냄
gd := instantDoc{
Gid: gid,
rh: gi.rh,
}
gi.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gd.strid(),
Body: bson.M{
gd.tid(accid): bson.M{},
},
Tag: []string{"MemberDocFull"},
})
gd.removeMember(accid)
if gd.Count == 0 {
gd.rh.Del(gd.rh.Context(), gd.strid()).Result()
}
}
}
func (gi *groupInstant) MakeClientClean(w http.ResponseWriter, r *http.Request) {
var accid primitive.ObjectID
if err := gocommon.MakeDecoder(r).Decode(&accid); err != nil {
logger.Println("MakeClientClean failed. decode returns err :", err)
return
}
gi.makeClientClean(accid)
}
func (gi *groupInstant) ClientDisconnected(msg string, callby *wshandler.Sender) {
gi.makeClientClean(callby.Accid)
}

553
core/group_memory.go Normal file
View File

@ -0,0 +1,553 @@
package core
import (
"context"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"net/url"
"time"
"github.com/go-redis/redis/v8"
"repositories.action2quare.com/ayo/gocommon"
"repositories.action2quare.com/ayo/gocommon/logger"
"repositories.action2quare.com/ayo/gocommon/wshandler"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type accountID = primitive.ObjectID
type ticketID = primitive.ObjectID
type groupID = primitive.ObjectID
func init() {
gob.Register(memberDoc{})
gob.Register(groupDoc{})
gob.Register(Invitation{})
gob.Register(InvitationFail{})
}
func makeTid(gid groupID, in accountID) string {
var out primitive.ObjectID
for i := range in {
out[12-i-1] = gid[i] ^ in[12-i-1]
}
return out.Hex()
}
type Invitation struct {
GroupID groupID `json:"_gid"`
TicketID string `json:"_tid"`
Inviter bson.M `json:"_inviter"` // memberDoc.Body
ExpireAtUTC int64 `json:"_expire_at_utc"`
}
// 플레이어한테 공유하는 멤버 정보
type memberDoc struct {
Body bson.M `json:"_body"`
Invite bool `json:"_invite"`
InviteExpire int64 `json:"_invite_exp"`
}
type InvitationFail bson.M
type groupDoc struct {
Members map[string]any `json:"_members"`
InCharge string `json:"_incharge"`
Gid string `json:"_gid"`
rh *gocommon.RedisonHandler
id groupID
}
func (gd *groupDoc) loadMemberFull(tid string) (bson.M, error) {
full, err := gd.rh.JSONGet(gd.strid(), "$._members."+tid)
if err != nil {
return nil, err
}
bt := []byte(full.(string))
bt = bt[1 : len(bt)-1]
var doc bson.M
if err = json.Unmarshal(bt, &doc); err != nil {
return nil, err
}
return doc, nil
}
func (gd *groupDoc) loadFull() (doc bson.M) {
// 새 멤버에 그룹 전체를 알림
full, err := gd.rh.JSONGet(gd.strid(), "$")
if err == nil {
bt := []byte(full.(string))
bt = bt[1 : len(bt)-1]
err = json.Unmarshal(bt, &doc)
if err != nil {
logger.Println("loadFull err :", err)
}
} else {
logger.Println("loadFull err :", err)
}
return
}
func (gd *groupDoc) strid() string {
if len(gd.Gid) == 0 {
gd.Gid = gd.id.Hex()
}
return gd.Gid
}
func (gd *groupDoc) tid(in accountID) string {
return makeTid(gd.id, in)
}
func (gd *groupDoc) mid(tid string) accountID {
tidobj, _ := primitive.ObjectIDFromHex(tid)
var out primitive.ObjectID
for i := range tidobj {
out[12-i-1] = gd.id[i] ^ tidobj[12-i-1]
}
return out
}
func (gd *groupDoc) addInvite(inviteeDoc bson.M, ttl time.Duration, max int) (*memberDoc, error) {
targetmid := inviteeDoc["_mid"].(accountID)
targetbody := inviteeDoc["body"].(bson.M)
// 초대 가능한 빈 자리가 있나
tids, err := gd.rh.JSONObjKeys(gd.strid(), "$._members")
if err != nil {
return nil, err
}
now := time.Now().UTC()
createNewDoc := func() *memberDoc {
return &memberDoc{
Body: targetbody,
Invite: true,
InviteExpire: now.Add(ttl).Unix(),
}
}
newtid := gd.tid(targetmid)
if len(tids) < max {
// 빈자리를 찾았다.
newdoc := createNewDoc()
_, err := gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
return newdoc, err
}
expires, err := gd.rh.JSONGetInt64(gd.strid(), "$._members.._invite_exp")
if err != nil {
return nil, err
}
var delpaths []string
for i, expire := range expires {
if expire < now.Unix() {
// 만료된 초대가 있네? 지우자
delpaths = append(delpaths, "$._members."+tids[i])
}
}
if len(delpaths) == 0 {
// 빈자리가 없다
return nil, nil
}
if err := gd.rh.JSONMDel(gd.strid(), delpaths); err != nil {
return nil, err
}
newdoc := createNewDoc()
_, err = gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
return newdoc, err
}
func (gd *groupDoc) addMember(mid accountID, doc bson.M) (bson.M, error) {
tid := gd.tid(mid)
prefix := "$._members." + tid
if _, err := gd.rh.JSONMerge(gd.strid(), prefix+"._body", doc, gocommon.RedisonSetOptionXX); err != nil {
return nil, err
}
if err := gd.rh.JSONMDel(gd.strid(), []string{prefix + "._invite", prefix + "._invite_exp"}); err != nil {
return nil, err
}
return gd.loadMemberFull(tid)
}
func (gd *groupDoc) removeMember(mid accountID) error {
_, err := gd.rh.JSONDel(gd.strid(), "$._members."+gd.tid(mid))
return err
}
func (gd *groupDoc) getMembers() (map[string]any, error) {
res, err := gd.rh.JSONGet(gd.strid(), "$._members")
if err != nil {
return nil, err
}
var temp []map[string]any
err = json.Unmarshal([]byte(res.(string)), &temp)
if err != nil {
return nil, err
}
out := make(map[string]any)
for k, v := range temp[0] {
body := v.(map[string]any)["_body"]
out[gd.mid(k).Hex()] = body
}
return out, nil
}
type groupInMemory struct {
*groupConfig
sendUpstreamMessage func(*wshandler.UpstreamMessage)
sendEnterRoomMessage func(groupID, accountID)
sendLeaveRoomMessage func(groupID, accountID)
rh *gocommon.RedisonHandler
}
func (gm *groupInMemory) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*groupDoc, error) {
tid := makeTid(newid, charge)
gd := &groupDoc{
Members: map[string]any{
tid: &memberDoc{
Body: chargeDoc,
Invite: false,
InviteExpire: 0,
},
},
InCharge: tid,
rh: gm.rh,
id: newid,
}
_, err := gm.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX)
if err != nil {
return nil, err
}
return gd, nil
}
func (gm *groupInMemory) find(id groupID) (*groupDoc, error) {
if id.IsZero() {
return nil, nil
}
_, err := gm.rh.JSONObjLen(id.Hex(), "$")
if err == redis.Nil {
return nil, nil
}
if err != nil {
return nil, err
}
return &groupDoc{
rh: gm.rh,
id: id,
}, nil
}
func (gm *groupInMemory) Create(form url.Values, base bson.M) (groupID, error) {
return primitive.NilObjectID, nil
}
func (gm *groupInMemory) Candidate(gid groupID, mid accountID, doc bson.M) error {
logger.Error("not implemented func : Canidate")
return nil
}
var errGroupNotExist = errors.New("group does not exist")
func (gm *groupInMemory) Join(gid groupID, mid accountID, doc bson.M) error {
gd, err := gm.find(gid)
if err != nil {
return err
}
if gd == nil {
// 그룹이 없다. 실패
return errGroupNotExist
}
// 내 정보 업데이트할 때에도 사용됨
if memdoc, err := gd.addMember(mid, doc); err == nil {
// 기존 유저에게 새 유저 알림
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: map[string]any{
gd.tid(mid): memdoc,
},
Tag: []string{"MemberDocFull"},
})
gm.sendEnterRoomMessage(gid, mid)
// 새 멤버에 그룹 전체를 알림
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "@" + mid.Hex(),
Body: gd.loadFull(),
Tag: []string{"GroupDocFull"},
})
}
return err
}
var errInviteeDocMidMissing = errors.New("inviteeDoc must have '_mid' field")
var errAlreadyInvited = errors.New("this target is already invited by someone or me")
func (gm *groupInMemory) Invite(gid groupID, mid accountID, inviterDoc bson.M, inviteeDoc bson.M) (string, error) {
targetid, ok := inviteeDoc["_mid"].(accountID)
if !ok {
return "", errInviteeDocMidMissing
}
// targetid에 초대한 mid가 들어있다.
already, err := gm.rh.Get(context.Background(), targetid.Hex()).Result()
if err != nil && err != redis.Nil {
return "", err
}
if len(already) > 0 {
if already != mid.Hex() {
// 이미 초대 중이다.
// inviter한테 알려줘야 한다.
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "@" + mid.Hex(),
Body: inviteeDoc,
Tag: []string{"InvitationFail"},
})
}
return "", errAlreadyInvited
}
gd, err := gm.find(gid)
if err != nil {
return "", err
}
if gd == nil {
gd, err = gm.createGroup(gid, mid, inviterDoc)
if err != nil {
return "", err
}
// 내가 wshandler room에 입장
gm.sendEnterRoomMessage(gid, mid)
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "@" + mid.Hex(),
Body: gd,
Tag: []string{"GroupDocFull"},
})
}
newdoc, err := gd.addInvite(inviteeDoc, time.Duration(gm.InviteExpire+1)*time.Second, gm.MaxMember)
if err != nil {
return "", err
}
// 초대 중 표시
_, err = gm.rh.SetNX(context.Background(), targetid.Hex(), mid.Hex(), time.Duration(gm.InviteExpire)*time.Second).Result()
if err != nil {
return "", err
}
// invitee에게 알림
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "@" + targetid.Hex(),
Body: Invitation{
GroupID: gid,
TicketID: gd.tid(targetid),
Inviter: inviterDoc,
ExpireAtUTC: newdoc.InviteExpire,
},
Tag: []string{"Invitation"},
})
return gd.strid(), nil
}
func (gm *groupInMemory) CancelInvitation(gid groupID, tid ticketID) error {
return nil
}
var errInvitationExpired = errors.New("invitation is already expired")
func (gm *groupInMemory) AcceptInvitation(gid groupID, mid accountID, member bson.M) error {
cnt, err := gm.rh.Del(context.Background(), mid.Hex()).Result()
if err != nil {
return err
}
if cnt == 0 {
// 만료됨
return errInvitationExpired
}
gd := &groupDoc{
id: gid,
rh: gm.rh,
}
memberDoc, err := gd.addMember(mid, member)
if err == nil {
// 기존 멤버에게 새 멤버를 알림
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: map[string]any{
gd.tid(mid): memberDoc,
},
Tag: []string{"MemberDocFull"},
})
gm.sendEnterRoomMessage(gid, mid)
// 새 멤버에 그룹 전체를 알림
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "@" + mid.Hex(),
Body: gd.loadFull(),
Tag: []string{"GroupDocFull"},
})
}
// 실패
return err
}
func (gm *groupInMemory) QueryGroupMembers(gid groupID) (bson.M, error) {
gd := groupDoc{
id: gid,
rh: gm.rh,
}
return gd.getMembers()
}
func (gm *groupInMemory) DenyInvitation(gid groupID, mid accountID, tid ticketID) error {
gm.rh.Del(context.Background(), mid.Hex()).Result()
gd := groupDoc{
id: gid,
rh: gm.rh,
}
return gd.removeMember(mid)
}
func (gm *groupInMemory) QueryInvitations(mid accountID, after primitive.Timestamp) ([]bson.M, error) {
return nil, nil
}
func (gm *groupInMemory) Exist(gid groupID, filter bson.M) (bool, error) {
return false, nil
}
func (gm *groupInMemory) FindAll(filter bson.M, projection string, after primitive.Timestamp) ([]bson.M, error) {
return nil, nil
}
func (gm *groupInMemory) FindOne(gid groupID, projection string) (bson.M, error) {
return nil, nil
}
func (gm *groupInMemory) Leave(gid groupID, mid accountID) error {
gd := groupDoc{
id: gid,
rh: gm.rh,
}
if err := gd.removeMember(mid); err != nil {
return err
}
// 나한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "@" + mid.Hex(),
Body: bson.M{"gid": gid},
Tag: []string{"GroupDocFull", gid.Hex()},
})
gm.sendLeaveRoomMessage(gid, mid)
return nil
}
func (gm *groupInMemory) UpdateMemberDocument(gid groupID, mid accountID, doc bson.M) error {
gd := &groupDoc{
id: gid,
rh: gm.rh,
}
prefixPath := fmt.Sprintf("$._members.%s.", gd.tid(mid))
err := gm.rh.JSONMSetRel(gd.strid(), prefixPath, doc)
if err != nil {
return err
}
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: map[string]any{
gd.tid(mid): doc,
},
Tag: []string{"MemberDocFragment"},
})
return nil
}
func (gm *groupInMemory) Dismiss(gid groupID) error {
return nil
}
func (gm *groupInMemory) UpdateGroupDocument(gid groupID, frag bson.M) error {
gd := groupDoc{
id: gid,
rh: gm.rh,
}
if err := gm.rh.JSONMSetRel(gd.strid(), "$.", frag); err != nil {
return err
}
// 업데이트 알림
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: frag,
Tag: []string{"GroupDocFragment"},
})
return nil
}
func (cfg *groupConfig) prepareInMemory(ctx context.Context, typename string, sub *subTavern) (group, error) {
// group document
// member document
region := sub.region
wsh := sub.wsh
storage := config.RegionStorage[sub.region]
redisClient, err := gocommon.NewRedisClient(storage.Redis["tavern"])
if err != nil {
return nil, err
}
// 여기서는 subscribe channel
// 각 함수에서는 publish
gm := &groupInMemory{
groupConfig: cfg,
rh: gocommon.NewRedisonHandler(ctx, redisClient),
sendUpstreamMessage: func(msg *wshandler.UpstreamMessage) {
wsh.SendUpstreamMessage(region, msg)
},
sendEnterRoomMessage: func(gid groupID, accid accountID) {
wsh.EnterRoom(region, gid.Hex(), accid)
},
sendLeaveRoomMessage: func(gid groupID, accid accountID) {
wsh.LeaveRoom(region, gid.Hex(), accid)
},
}
return gm, nil
}

View File

@ -1,979 +0,0 @@
package core
import (
"context"
"encoding/gob"
"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 partyDoc struct {
Members map[string]any `json:"_members"`
InCharge string `json:"_incharge"`
Gid string `json:"_gid"`
rh *gocommon.RedisonHandler
id groupID
}
func init() {
gob.Register(partyDoc{})
gob.Register(Invitation{})
}
func (gd *partyDoc) 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 *partyDoc) 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 *partyDoc) strid() string {
if len(gd.Gid) == 0 {
gd.Gid = gd.id.Hex()
}
return gd.Gid
}
func (gd *partyDoc) tid(in accountID) string {
return makeTid(gd.id, in)
}
func (gd *partyDoc) 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 *partyDoc) 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 *partyDoc) addMember(mid accountID, character bson.M, option string) (bson.M, error) {
tid := gd.tid(mid)
prefix := "$._members." + tid
if option == gocommon.RedisonSetOptionNX {
if _, err := gd.rh.JSONSet(gd.strid(), prefix, bson.M{"_body": character}, gocommon.RedisonSetOptionNX); err != nil {
return nil, err
}
} else {
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 *partyDoc) getIncharge() string {
if len(gd.InCharge) == 0 {
incharge, err := gd.rh.JSONGet(gd.strid(), "$._incharge")
if err == nil && incharge != nil {
gd.InCharge = strings.Trim(incharge.(string), "[]\"")
}
}
return gd.InCharge
}
func (gd *partyDoc) 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 *partyDoc) removeMember(mid accountID) error {
return gd.removeMemberByTid(gd.tid(mid))
}
func (gd *partyDoc) 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) {
gp.rh.JSONDel(accid.Hex(), "$.party.state")
tv.wsh.LeaveRoom(gid.Hex(), accid)
}
return nil
}
// 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
Character bson.M
Option string
}
if err := gocommon.MakeDecoder(r).Decode(&data); err != nil {
logger.Println("JoinParty failed. DecodeGob returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// Option : ["NX" | "XX" | ""]
character := data.Character
gid := data.Gid
mid := data.Mid
option := strings.ToUpper(data.Option)
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 {
// 그룹이 없다. 없을 수도 있지
gocommon.MakeEncoder(w, r).Encode("")
return
}
// 내 정보 업데이트할 때에도 사용됨
if option == "XX" {
// 이미 멤버여야 재입장 가능
path := "$._members." + gd.tid(mid) + "._body"
if _, err := gd.rh.JSONSet(gd.strid(), path, character, gocommon.RedisonSetOptionXX); err != nil {
// 멤버가 아니네? 그새 파티장이 쫓아냈을 수도 있다.
gocommon.MakeEncoder(w, r).Encode("")
return
}
}
gp.rh.JSONSet(mid.Hex(), "$.party", bson.M{"id": gid.Hex()})
memdoc, err := gd.addMember(mid, character, option)
if err != nil {
logger.Println("JoinParty failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 기존 유저에게 새 유저 알림
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"},
})
gocommon.MakeEncoder(w, r).Encode(gd.strid())
}
func (gp *groupParty) ForceClearPartyMember(w http.ResponseWriter, r *http.Request) {
var doc struct {
Gid primitive.ObjectID
Mid primitive.ObjectID
}
if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil {
logger.Println("ConditionalClearPartyMember failed. DecodeGob returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
gd := partyDoc{
id: doc.Gid,
rh: gp.rh,
}
gd.removeMember(doc.Mid)
}
func (gp *groupParty) ConditionalClearPartyMember(w http.ResponseWriter, r *http.Request) {
var doc struct {
Gid string
Mid string
}
// accid가 접속해 있지 않으면 파티에서 나간 걸로 간주하고
// accid가 접속해 있으면 아무것도 하지 않는다.
if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil {
logger.Println("ConditionalClearPartyMember failed. DecodeGob returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
pids, err := gp.rh.JSONGetString(doc.Mid, "$.party.id")
if err != nil {
logger.Println("ConditionalClearPartyMember failed. gp.rh.JSONGetString returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
removeMember := func() {
gid, _ := primitive.ObjectIDFromHex(doc.Gid)
mid, _ := primitive.ObjectIDFromHex(doc.Mid)
gd := partyDoc{
id: gid,
rh: gp.rh,
}
if gd.getIncharge() == gd.tid(mid) {
// 방장이 나갔다.
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + doc.Gid,
Body: bson.M{"gid": gid},
Tag: []string{"GroupDocFull", gid.Hex()},
})
gd.rh.Del(gd.rh.Context(), gd.strid()).Result()
} else {
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + doc.Gid,
Body: bson.M{
gd.tid(mid): bson.M{},
},
Tag: []string{"MemberDocFull"},
})
gd.removeMember(mid)
}
}
if len(pids) == 0 {
// 없다.
// doc.Gid에서 제거
removeMember()
} else if pids[0] != doc.Gid {
// 다른 파티? 기존 파티에서 제거
removeMember()
}
}
// 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
InitialPartyDoc 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, doc.InitialPartyDoc)
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"},
})
// 그룹에게 알림
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: bson.M{
gd.tid(targetid): bson.M{
"nickname": doc.Invitee["nickname"],
"_invite": true,
},
},
Tag: []string{"MemberDocFull"},
})
}
func (gp *groupParty) UpdatePartyMemberDocument(w http.ResponseWriter, r *http.Request) {
var doc struct {
Gid string
Tid string
Fragment bson.M
}
if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil {
logger.Println("UpdatePartyMemberDocument failed. DecodeGob returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
gidobj, _ := primitive.ObjectIDFromHex(doc.Gid)
mid := midFromTid(gidobj, doc.Tid)
gp.updateMemberDocument(gidobj, mid, doc.Fragment)
}
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.Println("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.Println("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 := &partyDoc{
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 := &partyDoc{
id: gid,
rh: gp.rh,
}
memberDoc, err := gd.addMember(mid, member, "XX")
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 := &partyDoc{
id: gid,
rh: gp.rh,
}
prefixPath := fmt.Sprintf("$._members.%s._body.", 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 := partyDoc{
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.Println("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 := partyDoc{
id: gid,
rh: gp.rh,
}
members, err := gd.getMembers()
if err != nil {
logger.Println("QueryPartyMembers failed. group.QueryPartyMembers returns err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
if err := gocommon.MakeEncoder(w, r).Encode(members); err != nil {
logger.Println("QueryPartyMembers failed. writeBsonDoc return err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (gp *groupParty) createGroup(newid groupID, charge accountID, chargeDoc bson.M, initialGroupDoc bson.M) (*partyDoc, error) {
tid := makeTid(newid, charge)
gd := &partyDoc{
Members: map[string]any{
tid: &memberDoc{
Body: chargeDoc,
Invite: false,
InviteExpire: 0,
},
},
InCharge: tid,
Gid: newid.Hex(),
rh: gp.rh,
id: newid,
}
var err error
if initialGroupDoc != nil {
initialGroupDoc["_members"] = gd.Members
initialGroupDoc["_incharge"] = gd.InCharge
initialGroupDoc["_gid"] = gd.Gid
_, err = gp.rh.JSONSet(gd.strid(), "$", initialGroupDoc, gocommon.RedisonSetOptionNX)
} else {
_, err = gp.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX)
}
if err != nil {
return nil, err
}
return gd, nil
}
func (gp *groupParty) find(id groupID) (*partyDoc, 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 &partyDoc{
rh: gp.rh,
id: id,
}, nil
}
func (gp *groupParty) ClientDisconnected(msg string, callby *wshandler.Sender) {
gids, _ := gp.rh.JSONGetString(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, callby.Accid)
if msg != "pending" {
gd := &partyDoc{
rh: gp.rh,
id: gid,
}
if gd.getIncharge() == gd.tid(callby.Accid) {
// 방장이 나감. 방 폭파
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gidstr,
Body: bson.M{"gid": gid},
Tag: []string{"GroupDocFull", gidstr},
})
gd.rh.Del(gd.rh.Context(), gd.strid()).Result()
} else {
// gid에는 제거 메시지 보냄
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gidstr,
Body: bson.M{
makeTid(gid, callby.Accid): bson.M{},
},
Tag: []string{"MemberDocFull"},
})
gd.removeMember(callby.Accid)
}
}
}
}
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)
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 := partyDoc{
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
}
if gd.getIncharge() == gd.tid(mid) {
// 방장이 나감. 방 폭파
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gidstr,
Body: bson.M{"gid": gid},
Tag: []string{"GroupDocFull", gidstr},
})
gd.rh.Del(gd.rh.Context(), gd.strid()).Result()
} else {
// 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"},
})
gd.removeMember(mid)
}
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 := partyDoc{
id: gid,
rh: gp.rh,
}
gd.removeMember(mid)
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gd.strid(),
Body: bson.M{
gd.tid(mid): bson.M{},
},
Tag: []string{"MemberDocFull"},
})
}

View File

@ -2,45 +2,95 @@ package core
import (
"context"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"reflect"
"strings"
"time"
"github.com/go-redis/redis/v8"
"github.com/gorilla/websocket"
"repositories.action2quare.com/ayo/gocommon"
"repositories.action2quare.com/ayo/gocommon/flagx"
"repositories.action2quare.com/ayo/gocommon/logger"
"repositories.action2quare.com/ayo/gocommon/metric"
"repositories.action2quare.com/ayo/gocommon/session"
"repositories.action2quare.com/ayo/gocommon/wshandler"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/primitive"
)
var devflag = flagx.Bool("dev", false, "")
const (
defaultMaxMemory = 32 << 10 // 32 KB
)
func writeBsonArr(w io.Writer, src []bson.M) error {
return writeBsonDoc(w, bson.M{
"r": src,
})
}
func writeBsonDoc[T any](w io.Writer, src T) error {
rw, err := bsonrw.NewBSONValueWriter(w)
if err != nil {
return err
}
enc, err := bson.NewEncoder(rw)
if err != nil {
return err
}
return enc.Encode(src)
}
func readBsonDoc(r io.Reader, src any) error {
body, err := io.ReadAll(r)
if err != nil {
return err
}
if len(body) == 0 {
return nil
}
decoder, err := bson.NewDecoder(bsonrw.NewBSONDocumentReader(body))
if err != nil {
return err
}
err = decoder.Decode(src)
if err != nil {
return err
}
return nil
}
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"`
EosClientId string `json:"eos_client_id"`
EosClientSecret string `json:"eos_client_secret"`
EosDeploymentId string `json:"eos_deployment_id"`
macAddr string
gocommon.RegionStorageConfig `json:",inline"`
GroupTypes map[string]*groupConfig `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.HttpApiBroker
subTaverns []*subTavern
wsh *wshandler.WebsocketHandler
}
type subTavern struct {
mongoClient gocommon.MongoClient
redisClient *redis.Client
wsh *wshandler.WebsocketHandler
region string
groups map[string]group
methods map[string]reflect.Method
}
func getMacAddr() (string, error) {
@ -60,14 +110,19 @@ func getMacAddr() (string, error) {
}
// New :
func New(context context.Context, wsh *wshandler.WebsocketHandler) (*Tavern, error) {
if err := gocommon.LoadConfig(&config); err != nil {
return nil, logger.ErrorWithCallStack(err)
func New(context context.Context, wsh *wshandler.WebsocketHandler, inconfig *TavernConfig) (*Tavern, error) {
if inconfig == nil {
var loaded TavernConfig
if err := gocommon.LoadConfig(&loaded); err != nil {
return nil, err
}
inconfig = &loaded
}
config = *inconfig
macaddr, err := getMacAddr()
if err != nil {
return nil, logger.ErrorWithCallStack(err)
return nil, err
}
config.macAddr = macaddr
tv := &Tavern{
@ -76,110 +131,141 @@ func New(context context.Context, wsh *wshandler.WebsocketHandler) (*Tavern, err
if err = tv.prepare(context); err != nil {
logger.Println("tavern prepare() failed :", err)
return nil, logger.ErrorWithCallStack(err)
return nil, err
}
return tv, nil
}
var ccu = metric.MetricWriterNil
func (tv *Tavern) Cleanup() {
ccu.Set(0)
tv.mongoClient.Close()
for _, st := range tv.subTaverns {
st.mongoClient.Close()
}
}
func (tv *Tavern) prepare(ctx context.Context) error {
redisClient, err := gocommon.NewRedisClient(config.RedisURL)
if err != nil {
return logger.ErrorWithCallStack(err)
}
for region, addr := range config.RegionStorage {
var dbconn gocommon.MongoClient
var err error
var groupinstance group
tv.redison = gocommon.NewRedisonHandler(redisClient.Context(), redisClient)
tv.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(tv, "tv"))
if cfg, ok := config.Group["chat"]; ok {
chat := new(groupChat)
if err := chat.Initialize(tv, cfg); err != nil {
return logger.ErrorWithCallStack(err)
var tmp *subTavern
methods := make(map[string]reflect.Method)
tp := reflect.TypeOf(tmp)
for i := 0; i < tp.NumMethod(); i++ {
method := tp.Method(i)
methods[method.Name] = method
}
tv.httpApiBorker.AddHandler(gocommon.MakeHttpApiHandler(chat, "chat"))
tv.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(chat, "chat"))
}
if cfg, ok := config.Group["party"]; ok {
party := new(groupParty)
if err := party.Initialize(tv, cfg); err != nil {
return logger.ErrorWithCallStack(err)
redisClient, err := gocommon.NewRedisClient(addr.Redis["tavern"])
if err != nil {
return err
}
tv.httpApiBorker.AddHandler(gocommon.MakeHttpApiHandler(party, "party"))
tv.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(party, "party"))
sub := &subTavern{
wsh: tv.wsh,
mongoClient: dbconn,
redisClient: redisClient,
region: region,
methods: methods,
}
groups := make(map[string]group)
for typename, cfg := range config.GroupTypes {
cfg.Name = typename
if cfg.Transient {
groupinstance, err = cfg.prepareInMemory(ctx, typename, sub)
//} else {
// TODO : db
// if !dbconn.Connected() {
// dbconn, err = gocommon.NewMongoClient(ctx, url.Mongo, region)
// if err != nil {
// return err
// }
// }
// groupinstance, err = cfg.preparePersistent(ctx, region, dbconn, tv.wsh)
}
if err != nil {
return err
}
groups[typename] = groupinstance
}
sub.groups = groups
tv.subTaverns = append(tv.subTaverns, sub)
}
instant := new(groupInstant)
if err := instant.Initialize(tv); err != nil {
return logger.ErrorWithCallStack(err)
}
tv.httpApiBorker.AddHandler(gocommon.MakeHttpApiHandler(instant, "instant"))
tv.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(instant, "instant"))
ccu = metric.NewMetric(metric.MetricGuage, "concurrent_user", "concurrent user count", map[string]string{"game": "lobby"})
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)
for _, sub := range tv.subTaverns {
tv.wsh.RegisterReceiver(sub.region, sub)
var pattern string
if sub.region == "default" {
pattern = gocommon.MakeHttpHandlerPattern(prefix, "api")
} else {
pattern = gocommon.MakeHttpHandlerPattern(prefix, sub.region, "api")
}
serveMux.HandleFunc(pattern, sub.api)
}
return nil
}
func (tv *Tavern) EnterChannel(ctx wshandler.ApiCallContext) {
tv.wsh.EnterRoom(ctx.Arguments[0].(string), ctx.CallBy.Accid)
func (sub *subTavern) OnClientMessageReceived(sender *wshandler.Sender, messageType wshandler.WebSocketMessageType, body io.Reader) {
if messageType == wshandler.Connected {
logger.Println("OnClientMessageReceived : connected ", sender.Accid.Hex())
} else if messageType == wshandler.Disconnected {
logger.Println("OnClientMessageReceived : disconnected ", sender.Accid.Hex())
} else if messageType == wshandler.BinaryMessage {
var msg map[string][]any
dec := json.NewDecoder(body)
if err := dec.Decode(&msg); err == nil {
for cmd, args := range msg {
switch cmd {
case "EnterChannel":
sub.wsh.EnterRoom(sub.region, args[0].(string), sender.Accid)
case "LeaveChannel":
sub.wsh.LeaveRoom(sub.region, args[0].(string), sender.Accid)
case "UpdateGroupMemberDocument":
typename := args[0].(string)
gidobj, _ := primitive.ObjectIDFromHex(args[1].(string))
doc := args[2].(map[string]any)
if group := sub.groups[typename]; group != nil {
group.UpdateMemberDocument(gidobj, sender.Accid, doc)
}
case "UpdateGroupDocument":
typename := args[0].(string)
gidobj, _ := primitive.ObjectIDFromHex(args[1].(string))
doc := args[2].(map[string]any)
if group := sub.groups[typename]; group != nil {
group.UpdateGroupDocument(gidobj, doc)
}
}
}
}
}
}
func (tv *Tavern) LeaveChannel(ctx wshandler.ApiCallContext) {
tv.wsh.LeaveRoom(ctx.Arguments[0].(string), ctx.CallBy.Accid)
}
func (tv *Tavern) ClientConnected(conn *websocket.Conn, callby *wshandler.Sender) {
ccu.Add(1)
tv.redison.Del(tv.redison.Context(), callby.Accid.Hex())
_, err := tv.redison.JSONSet(callby.Accid.Hex(), "$", bson.M{"_ts": time.Now().UTC().Unix()})
func (sub *subTavern) OnRoomCreated(region, name string) {
_, err := sub.redisClient.Persist(context.Background(), name).Result()
if err != nil {
logger.Println("OnClientMessageReceived HSet error :", err)
logger.Println("OnRoomCreate Persist failed :", err)
}
}
func (tv *Tavern) ClientDisconnected(msg string, callby *wshandler.Sender) {
ccu.Add(-1)
tv.redison.Del(tv.redison.Context(), callby.Accid.Hex()).Result()
}
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()
func (sub *subTavern) OnRoomDestroyed(region, name string) {
_, err := sub.redisClient.Expire(context.Background(), name, 3600*time.Second).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, "$")
logger.Println("OnRoomDestroyed Persist failed :", err)
}
}
func (tv *Tavern) api(w http.ResponseWriter, r *http.Request) {
func (sub *subTavern) api(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
@ -198,5 +284,29 @@ func (tv *Tavern) api(w http.ResponseWriter, r *http.Request) {
return
}
tv.httpApiBorker.Call(w, r)
operation := r.URL.Query().Get("operation")
if len(operation) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
method, ok := sub.methods[operation]
if !ok {
// 없는 operation
logger.Println("fail to call api. operation is not valid :", operation)
w.WriteHeader(http.StatusBadRequest)
return
}
if r.PostForm == nil {
r.ParseMultipartForm(defaultMaxMemory)
}
args := []reflect.Value{
reflect.ValueOf(sub),
reflect.ValueOf(w),
reflect.ValueOf(r),
}
method.Func.Call(args)
}

View File

@ -3,7 +3,6 @@ package core
import (
"context"
"encoding/binary"
"fmt"
"testing"
"time"
@ -31,46 +30,11 @@ func TestPubSub(t *testing.T) {
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{
@ -84,7 +48,7 @@ func TestReJSON(t *testing.T) {
},
}
gd := partyDoc{
gd := groupDoc{
id: primitive.NewObjectID(),
}
@ -100,7 +64,7 @@ func TestReJSON(t *testing.T) {
logger.Println(rh.JSONGetInt64("jsontest", "$.members..exp"))
logger.Println(rh.JSONObjKeys("jsontest", "$.members"))
err = rh.JSONMSet("jsontest", map[string]any{
err := rh.JSONMSet("jsontest", map[string]any{
"$.members.mid1.key": "newval",
"$.members.mid2.key": "newval",
})

15
go.mod
View File

@ -1,30 +1,29 @@
module repositories.action2quare.com/ayo/tavern
go 1.22.1
go 1.20
require (
github.com/go-redis/redis/v8 v8.11.5
go.mongodb.org/mongo-driver v1.11.7
repositories.action2quare.com/ayo/gocommon v0.0.0-20250805124812-38a3da271a8e
repositories.action2quare.com/ayo/gocommon v0.0.0-20230719003337-29b2f258507d
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-cmp v0.5.4 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/klauspost/compress v1.16.6 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)

73
go.sum
View File

@ -1,52 +1,41 @@
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@ -70,21 +59,20 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -92,8 +80,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -101,23 +88,47 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
repositories.action2quare.com/ayo/gocommon v0.0.0-20250805124037-fb3f0385067a h1:NkFjJVZTNO6HsivUEikISfqF4O5zHk0VRdKOEOVTOG4=
repositories.action2quare.com/ayo/gocommon v0.0.0-20250805124037-fb3f0385067a/go.mod h1:q64I6gqlD61qwi9FfuPkwqy6Z6uzSHdcEjoHAJC27gQ=
repositories.action2quare.com/ayo/gocommon v0.0.0-20250805124812-38a3da271a8e h1:jxfkCjnDBPg4zw5oqecEdxjclpiBysBtNvI2ENqeG4E=
repositories.action2quare.com/ayo/gocommon v0.0.0-20250805124812-38a3da271a8e/go.mod h1:q64I6gqlD61qwi9FfuPkwqy6Z6uzSHdcEjoHAJC27gQ=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230715080833-f0f459332d1a h1:n2FF/GQYtCsi57Lh5m9LyQ2IZQ8pIppscBzhpvugmZg=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230715080833-f0f459332d1a/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230716073702-8f6c87a8aeb8 h1:+wfozysATxEl9NOm03gUF7/kpAr3Chxjn5wjCnJsfQw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230716073702-8f6c87a8aeb8/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230716093911-66aea48fb732 h1:Aq4E8kn1mN5z4ZpRYo5VFj2KektVNrTTuk0HocYMDCk=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230716093911-66aea48fb732/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230717084540-29843802ff0e h1:/eG6tAQzEaN178Aib+/erjHrE/+IjIVLRSmP4gx6D7E=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230717084540-29843802ff0e/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718004527-4b35e0e6386b h1:K3YQXnVP/W6LzwGzqOxwKmFUD5IrrNPEWYcN/fSinck=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718004527-4b35e0e6386b/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718005518-289af24a8ffa h1:YmzJ1YccK3BxC/NbfB11SEUG1S6Lkz6ejg4kS3q/3Qc=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718005518-289af24a8ffa/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718020415-82abcddb497b h1:baO9csa0Esnp7UW+L8zJW/ygpjGHRve4rU2/1pVvXQg=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718020415-82abcddb497b/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718020838-c21017d2cd8b h1:FqLKDrFji0+giFwAJ3oV6dIOR6Sd/aaay76WgWIEVR8=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718020838-c21017d2cd8b/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718032106-40a603522d40 h1:VyFfS0d6pTX2HbZoDHOxJwag4aVSLOh/LrQXqfSJLBg=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718032106-40a603522d40/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718083804-d724cc84fa94 h1:iQPrRcZ6XfFblpVHxe/CIoWyTj7imF+3edIGSX6ZMM8=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718083804-d724cc84fa94/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718084512-89fa9e4ac585 h1:Wy6qjZ0uHfp02/H688zotRfzYGRPjun7Qay0Z9B/hSg=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718084512-89fa9e4ac585/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718105124-72a683fed2c0 h1:8LmRo2nKaLi4QCmO/agSpNTmCD0EdwFycjHjOweQJp8=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230718105124-72a683fed2c0/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230719003101-256bfd030c29 h1:ADScrqJgmk/TfyOu/6oXD3WkSH8sh3Bw360O8GKuEV8=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230719003101-256bfd030c29/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230719003337-29b2f258507d h1:eMzrvVkQfbs5X5dcw80TGGKtJ+6XELl7zNsWiuq4gzs=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230719003337-29b2f258507d/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=

24
main.go
View File

@ -9,9 +9,8 @@ import (
"repositories.action2quare.com/ayo/gocommon/wshandler"
"repositories.action2quare.com/ayo/tavern/core"
"repositories.action2quare.com/ayo/gocommon"
common "repositories.action2quare.com/ayo/gocommon"
"repositories.action2quare.com/ayo/gocommon/logger"
"repositories.action2quare.com/ayo/gocommon/session"
)
var prefix = flagx.String("prefix", "", "")
@ -21,36 +20,27 @@ func main() {
ctx, cancel := context.WithCancel(context.Background())
var config core.TavernConfig
if err := gocommon.LoadConfig(&config); err != nil {
if err := common.LoadConfig(&config); err != nil {
panic(err)
}
consumer, err := session.NewConsumerWithConfig(ctx, config.SessionConfig)
wsh, err := wshandler.NewWebsocketHandler()
if err != nil {
panic(err)
}
wsh, err := wshandler.NewWebsocketHandler(consumer, config.RedisURL)
if err != nil {
panic(err)
}
if tv, err := core.New(ctx, wsh); err != nil {
if tv, err := core.New(ctx, wsh, &config); err != nil {
panic(err)
} else {
serveMux := http.NewServeMux()
wsh.RegisterHandlers(serveMux, *prefix)
tv.RegisterHandlers(ctx, serveMux, *prefix)
server := gocommon.NewHTTPServer(serveMux)
server := common.NewHTTPServer(serveMux)
logger.Println("tavern is started")
wsh.Start(ctx)
if err := server.Start(); err != nil {
logger.Println(err)
}
server.Start()
cancel()
wsh.Cleanup()
tv.Cleanup()
wsh.Cleanup()
}
}