Compare commits

..

30 Commits

Author SHA1 Message Date
a9b0cf2493 파티 초대 accept와 deny 분리 2023-09-08 16:09:12 +09:00
3dde7ccaf5 bson을 json으로 통일(일단은) 2023-09-08 15:29:01 +09:00
1d14fb659d 모듈 업데이트 2023-09-08 11:50:52 +09:00
ce50657734 WebsocketApiHandler로 변경 2023-09-08 11:37:50 +09:00
4a51f7d433 서버간 api 호출 간소화 2023-09-05 17:14:07 +09:00
6410056c87 모듈 업데이트 2023-09-04 15:31:35 +09:00
f7173a4f49 파티를 옮길 때 기존 파티에 남아있는 거 처럼 보이는 문제 수정 2023-09-04 10:24:17 +09:00
9b0c4a121a body와 query 분리, 모듈 업데이트 2023-09-01 12:33:22 +09:00
e0504f688a 모듈 업데이트 2023-08-31 21:17:51 +09:00
fd1502e52a session consumer로 교체 2023-08-31 21:02:19 +09:00
cb5cd280b9 모듈 업데이트 2023-08-23 23:02:37 +09:00
a4923fa0a1 모듈 업데이트 2023-08-23 22:44:55 +09:00
2534aa2a36 파티 초대 실패 수정 2023-08-23 20:06:21 +09:00
a404764abf 모듈 업데이트 2023-08-14 21:42:56 +09:00
9a734f9f4d 타입 변경 2023-08-14 15:34:55 +09:00
884fb0080f UpdateChannelDocument 제거 2023-08-14 14:44:56 +09:00
a08353a920 chat document update 2023-08-12 15:26:32 +09:00
5e953d6131 모듈 업데이트 2023-08-11 19:26:44 +09:00
3d2ed40b1e config.json 추가 2023-08-02 17:34:09 +09:00
9de686e828 BroadcastMessageOnChannel api 추가 2023-08-02 16:22:18 +09:00
12ddd2cbfb 쫓아내기 버그 수정 / 채팅 채널 expire되는 문제 수정 2023-08-02 00:55:36 +09:00
2b0e60a06a 모듈 업데이트 2023-08-01 14:19:25 +09:00
922f55740b 모듈 업데이트 2023-07-28 15:36:39 +09:00
b6262515e0 대상 채널 선택 2023-07-27 21:57:26 +09:00
90d0fd319d 채팅 채널 입장 추가 2023-07-27 17:45:51 +09:00
310397dd2b party로 그룹 변경 2023-07-25 18:11:02 +09:00
bb6a741d63 멤버 접속종료 알림 2023-07-20 01:36:55 +09:00
4f1c79d3b7 접속 종료를 그룹에 알림 2023-07-19 17:02:49 +09:00
07cb4848fe gocommon master로 변경 2023-07-19 10:12:22 +09:00
8dded8b907 Squashed commit of the following:
commit 8e1b232d57
Author: mountain <mountain@action2quare.com>
Date:   Wed Jul 19 09:37:02 2023 +0900

    InMemory 그룹을 redis로 변경

commit 01da5bb3a4
Author: mountain <mountain@action2quare.com>
Date:   Tue Jul 18 01:31:39 2023 +0900

    body를 marshaling하고 클라이언트에서 flatten함

commit ba61a11659
Author: mountain <mountain@action2quare.com>
Date:   Mon Jul 17 17:47:07 2023 +0900

    gob 등록

commit 67cca13326
Author: mountain <mountain@action2quare.com>
Date:   Sun Jul 16 18:41:24 2023 +0900

    모듈 업데이트

commit 272c696c59
Author: mountain <mountain@action2quare.com>
Date:   Sun Jul 16 17:29:21 2023 +0900

    json value 다시 되돌림

commit aa568ec3fa
Author: mountain <mountain@action2quare.com>
Date:   Sun Jul 16 17:26:19 2023 +0900

    SetOption 타입 변경

commit b9c4d8b21b
Author: mountain <mountain@action2quare.com>
Date:   Sun Jul 16 17:15:08 2023 +0900

    objvalue marshalling 수정

commit 99834c1461
Author: mountain <mountain@action2quare.com>
Date:   Sun Jul 16 17:01:06 2023 +0900

    objlen 수정

commit 592112219e
Author: mountain <mountain@action2quare.com>
Date:   Sun Jul 16 16:38:05 2023 +0900

    gocommon 업데이트

commit 62485b6d54
Author: mountain <mountain@action2quare.com>
Date:   Sun Jul 16 15:36:20 2023 +0900

    redis json 마이그레이션 완료

commit d36dd13bb7
Author: mountain <mountain@action2quare.com>
Date:   Sun Jul 16 02:51:41 2023 +0900

    redis stack 사용
2023-07-19 09:37:40 +09:00
13 changed files with 1406 additions and 1404 deletions

1
.gitignore vendored
View File

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

View File

@ -5,68 +5,40 @@
"redis": {
"cache": "redis://192.168.8.94:6380/0",
"session": "redis://192.168.8.94:6380/1",
"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"
"tx": "redis://192.168.8.94:6380/2",
"tavern": "redis://192.168.8.94:6380/3",
"wshandler": "redis://192.168.8.94:6380/4"
}
}
},
"maingate_mongodb_url": "mongodb://192.168.8.94:27017/?replicaSet=repl01&retrywrites=false",
"maingate_service_url": "http://localhost/maingate",
"maingate_session_storage": "redis://192.168.8.94:6380/1",
"maingate_session_ttl" : 3600,
"maingate_api_token": "63d08aa34f0162622c11284b",
"tavern_service_url": "http://localhost/tavern",
"tavern_group_types": {
"subjugate": {
"text_search_field": [
"name"
],
"unique_index": [
"name,_id",
"_id,members",
"name,hidden"
],
"search_index": [
"rules"
],
"member_index": [
"_gid,candidate,luts",
"_gid,luts",
"_gid,expiring"
],
"invite_ttl": 30,
"candidate_ttl": 3600,
"invitee_exlusive": true,
"invitee_is_member": true,
"max_member": 4
},
"lobby": {
"party": {
"max_member": 3,
"invitee_exlusive": true,
"invitee_is_member": true,
"transient": true,
"invite_ttl": 30
},
"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)"
}
},
"services": {
"kingdom": {
"개발중": {
"url": "http://localhost/warehouse/dev",
"development": true
},
"개인서버": {
"url": "http://localhost/warehouse/private",
"development": false
}
}
}

View File

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

View File

@ -1 +0,0 @@
{}

View File

@ -1,42 +1,3 @@
package core
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)
}
type configDocument map[string]any

402
core/group_chat.go Normal file
View File

@ -0,0 +1,402 @@
package core
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/go-redis/redis/v8"
"repositories.action2quare.com/ayo/gocommon"
"repositories.action2quare.com/ayo/gocommon/logger"
"repositories.action2quare.com/ayo/gocommon/wshandler"
)
type channelID = string
type channelConfig struct {
Capacity int64 `json:"capacity"`
Size int64 `json:"size"`
Key string `json:"key"`
emptyJson string
}
type chatConfig struct {
DefaultCapacity int64 `json:"default_capacity"`
Channels map[string]*channelConfig `json:"channels"`
}
type groupChat struct {
chatConfig
rh *gocommon.RedisonHandler
enterRoom func(channelID, accountID)
leaveRoom func(channelID, accountID)
sendUpstreamMessage func(msg *wshandler.UpstreamMessage)
}
func (gc *groupChat) Initialize(tv *Tavern, cfg configDocument) error {
rem, _ := json.Marshal(cfg)
if err := json.Unmarshal(rem, &gc.chatConfig); err != nil {
return err
}
gc.enterRoom = func(chanid channelID, accid accountID) {
tv.wsh.EnterRoom(string(chanid), accid)
}
gc.leaveRoom = func(chanid channelID, accid accountID) {
tv.wsh.LeaveRoom(string(chanid), accid)
}
gc.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) {
tv.wsh.SendUpstreamMessage(msg)
}
gc.rh = tv.redison
for name, cfg := range gc.chatConfig.Channels {
if cfg.Capacity == 0 {
cfg.Capacity = gc.chatConfig.DefaultCapacity
}
cfg.Key = name
cfg.Size = 0
jm, _ := json.Marshal(cfg)
cfg.emptyJson = fmt.Sprintf("[%s]", string(jm))
_, err := gc.rh.JSONSet(name, "$", cfg)
if *devflag && err != nil {
gc.rh.JSONDel(name, "$")
_, err = gc.rh.JSONSet(name, "$", cfg)
}
if err != nil {
return err
}
}
return nil
}
func (gc *groupChat) ClientConnected(ctx wshandler.ApiCallContext) {
gc.rh.JSONSet(ctx.CallBy.Accid.Hex(), "$.channel", map[string]any{})
}
func (gc *groupChat) ClientDisconnected(ctx wshandler.ApiCallContext) {
docs, _ := gc.rh.JSONGetDocuments(ctx.CallBy.Accid.Hex(), "$.channel")
if len(docs) > 0 {
for k, v := range docs[0] {
typename := k
chanid := v.(string)
gc.leaveRoom(chanid, ctx.CallBy.Accid)
if k == "public" {
gc.rh.JSONNumIncrBy(chanid, "$.size", -1)
} else {
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + chanid,
Body: map[string]any{"sender": ctx.CallBy.Alias, "typename": typename},
Tag: []string{"LeavePrivateChannel"},
})
}
}
}
}
func (gc *groupChat) EnterPublicChannel(ctx wshandler.ApiCallContext) {
chanid := ctx.Arguments[0].(string)
if cfg, ok := gc.chatConfig.Channels[chanid]; ok {
size, err := gc.rh.JSONGetInt64(chanid, "$.size")
if err != nil || len(size) == 0 {
logger.Println("JSONGetInt64 failed :", chanid, err)
} else if size[0] < cfg.Capacity {
// 입장
newsize, err := gc.rh.JSONNumIncrBy(chanid, "$.size", 1)
if err == nil {
gc.enterRoom(chanid, ctx.CallBy.Accid)
gc.rh.JSONSet(ctx.CallBy.Accid.Hex(), "$.channel.public", chanid)
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + chanid,
Body: map[string]any{"size": newsize[0]},
Tag: []string{"ChattingChannelProperties"},
})
}
} else {
// 풀방
logger.Println("chatting channel is full :", chanid, size, cfg.Capacity)
}
} else {
logger.Println("chatting channel not valid :", chanid)
}
}
func (gc *groupChat) LeavePublicChannel(ctx wshandler.ApiCallContext) {
chanid := ctx.Arguments[0].(string)
cnt, _ := gc.rh.JSONDel(ctx.CallBy.Accid.Hex(), "$.channel.public")
if cnt > 0 {
gc.leaveRoom(chanid, ctx.CallBy.Accid)
gc.rh.JSONNumIncrBy(chanid, "$.size", -1)
}
}
func (gc *groupChat) TextMessage(ctx wshandler.ApiCallContext) {
chanid := ctx.Arguments[0].(string)
msg := ctx.Arguments[1].(string)
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + chanid,
Body: map[string]any{"sender": ctx.CallBy.Alias, "msg": msg},
Tag: []string{"TextMessage"},
})
}
func (gc *groupChat) EnterPrivateChannel(ctx wshandler.ApiCallContext) {
typename := ctx.Arguments[0].(string)
channel := ctx.Arguments[1].(string)
var reason string
if len(ctx.Arguments) > 2 {
reason = ctx.Arguments[2].(string)
}
if len(reason) > 0 {
// 수락
ok, err := gc.rh.JSONSet(ctx.CallBy.Accid.Hex(), "$.channel."+typename, channel, gocommon.RedisonSetOptionNX)
if err != nil || !ok {
// 이미 다른 private channel 참여 중
logger.Println("EnterPrivateChannel failed. HSetNX return err :", err, ctx.CallBy.Accid.Hex(), typename, channel)
return
}
gc.enterRoom(channel, ctx.CallBy.Accid)
} else {
// 내가 이미 private channel에 있다는 것을 다른 사람들에게 알려주기 위함
}
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + channel,
Body: map[string]any{
"sender": ctx.CallBy.Alias,
"msg": reason,
"typename": typename,
},
Tag: []string{"EnterPrivateChannel"},
})
}
func (gc *groupChat) LeavePrivateChannel(ctx wshandler.ApiCallContext) {
typename := ctx.Arguments[0].(string)
chanid := ctx.Arguments[1].(string)
cnt, _ := gc.rh.JSONDel(ctx.CallBy.Accid.Hex(), "$.channel."+typename)
if cnt > 0 {
gc.leaveRoom(chanid, ctx.CallBy.Accid)
gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + chanid,
Body: map[string]any{"sender": ctx.CallBy.Alias, "typename": typename},
Tag: []string{"LeavePrivateChannel"},
})
}
}
// func (gc *groupChat) ClientMessageReceived(sender *wshandler.Sender, mt wshandler.WebSocketMessageType, message any) {
// if mt == wshandler.Disconnected {
// if _, err := gc.rh.Del(gc.rh.Context(), accidHex(sender.Accid)).Result(); err != nil {
// logger.Println(err)
// }
// } else if mt == wshandler.BinaryMessage {
// commandline := message.([]any)
// cmd := commandline[0].(string)
// args := commandline[1:]
// switch cmd {
// case "EnterPublicChannel":
// chanid := args[0].(string)
// if cfg, ok := gc.chatConfig.Channels[chanid]; ok {
// size, err := gc.rh.JSONGetInt64(chanid, "$.size")
// if err != nil || len(size) == 0 {
// logger.Println("JSONGetInt64 failed :", chanid, err)
// } else if size[0] < cfg.Capacity {
// // 입장
// newsize, err := gc.rh.JSONNumIncrBy(chanid, "$.size", 1)
// if err == nil {
// gc.enterRoom(chanid, sender.Accid)
// sender.RegistDisconnectedCallback(chanid, func() {
// size, err := gc.rh.JSONNumIncrBy(chanid, "$.size", -1)
// if err == nil {
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
// Target: "#" + chanid,
// Body: map[string]any{"size": size},
// Tag: []string{"ChattingChannelProperties"},
// })
// }
// })
// gc.rh.HSet(gc.rh.Context(), accidHex(sender.Accid), "cc_pub", chanid)
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
// Target: "#" + chanid,
// Body: map[string]any{"size": newsize[0]},
// Tag: []string{"ChattingChannelProperties"},
// })
// }
// } else {
// // 풀방
// logger.Println("chatting channel is full :", chanid, size, cfg.Capacity)
// }
// } else {
// logger.Println("chatting channel not valid :", chanid)
// }
// case "LeavePublicChannel":
// chanid := args[0].(string)
// gc.rh.HDel(gc.rh.Context(), accidHex(sender.Accid), "cc_pub")
// gc.leaveRoom(chanid, sender.Accid)
// if f := sender.PopDisconnectedCallback(chanid); f != nil {
// f()
// }
// case "TextMessage":
// chanid := args[0].(string)
// msg := args[1].(string)
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
// Target: "#" + chanid,
// Body: map[string]any{"sender": sender.Alias, "msg": msg},
// Tag: []string{"TextMessage"},
// })
// case "EnterPrivateChannel":
// typename := args[0].(string)
// channel := args[1].(string)
// var reason string
// if len(args) > 2 {
// reason = args[2].(string)
// }
// if len(reason) > 0 {
// // 수락
// // 이거 HSet 하면 안되겠는데? JSONSet해야할 듯?
// ok, err := gc.rh.HSetNX(gc.rh.Context(), accidHex(sender.Accid), "cc_"+typename, channel).Result()
// if err != nil || !ok {
// // 이미 다른 private channel 참여 중
// logger.Println("EnterPrivateChannel failed. HSetNX return err :", err, sender.Accid.Hex(), typename, channel)
// return
// }
// gc.enterRoom(channel, sender.Accid)
// sender.RegistDisconnectedCallback(channel, func() {
// gc.rh.JSONDel(channel, "$."+sender.Accid.Hex())
// // 이거 HDel 하면 안되겠는데? JSONDel해야할 듯?
// cnt, _ := gc.rh.HDel(gc.rh.Context(), accidHex(sender.Accid), "cc_"+typename).Result()
// if cnt > 0 {
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
// Target: "#" + channel,
// Body: map[string]any{"sender": sender.Alias, "typename": typename},
// Tag: []string{"LeavePrivateChannel"},
// })
// }
// })
// } else {
// // 내가 이미 private channel에 있다는 것을 다른 사람들에게 알려주기 위함
// }
// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{
// Target: "#" + channel,
// Body: map[string]any{
// "sender": sender.Alias,
// "msg": reason,
// "typename": typename,
// },
// Tag: []string{"EnterPrivateChannel"},
// })
// case "LeavePrivateChannel":
// channel := args[1].(string)
// gc.leaveRoom(channel, sender.Accid)
// if f := sender.PopDisconnectedCallback(channel); f != nil {
// f()
// }
// }
// }
// }
func (gc *groupChat) FetchChattingChannels(w http.ResponseWriter, r *http.Request) {
var data struct {
Prefix string `bson:"prefix"`
}
if err := gocommon.ReadJsonDocumentFromBody(r.Body, &data); err != nil {
logger.Println("FetchChattingChannels failed. ReadJsonDocumentFromBody returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
prefix := data.Prefix
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 {
rows = append(rows, onechan.(string))
}
}
if len(rows) == 0 {
w.Write([]byte("[]"))
} else if len(rows) == 1 {
w.Write([]byte(rows[0]))
} else {
first := rows[0]
w.Write([]byte(first[:len(first)-1]))
for i := 1; i < len(rows); i++ {
mid := rows[i]
w.Write([]byte(","))
w.Write([]byte(mid[1 : len(mid)-1]))
}
w.Write([]byte("]"))
}
}
func (gc *groupChat) QueryPlayerChattingChannel(w http.ResponseWriter, r *http.Request) {
var data struct {
Accid string `bson:"accid"`
Typename string `bson:"typename"`
}
err := gocommon.ReadJsonDocumentFromBody(r.Body, &data)
if err != nil {
logger.Println("QueryPlayerChattingChannel failed. ReadJsonDocumentFromBody returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
accid := data.Accid
sub, err := gc.rh.JSONGetDocuments(accid, "$.channel")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if len(sub) > 0 {
enc := json.NewEncoder(w)
enc.Encode(sub[0])
}
}
func (gc *groupChat) SendMessageOnChannel(w http.ResponseWriter, r *http.Request) {
var msg wshandler.UpstreamMessage
if err := gocommon.ReadJsonDocumentFromBody(r.Body, &msg); err != nil {
logger.Println("SendMessageOnChannel failed. ReadJsonDocumentFromBody return err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
gc.sendUpstreamMessage(&msg)
}

View File

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

843
core/group_party.go Normal file
View File

@ -0,0 +1,843 @@
package core
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-redis/redis/v8"
"repositories.action2quare.com/ayo/gocommon"
"repositories.action2quare.com/ayo/gocommon/logger"
"repositories.action2quare.com/ayo/gocommon/wshandler"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type accountID = primitive.ObjectID
type groupID = primitive.ObjectID
func makeTid(gid groupID, in accountID) string {
var out primitive.ObjectID
for i := range in {
out[12-i-1] = gid[i] ^ in[12-i-1]
}
return out.Hex()
}
func midFromTid(gid groupID, in string) accountID {
h, _ := primitive.ObjectIDFromHex(in)
var out accountID
for i := range h {
out[12-i-1] = gid[i] ^ h[12-i-1]
}
return out
}
type Invitation struct {
GroupID groupID `json:"_gid"`
TicketID string `json:"_tid"`
Inviter bson.M `json:"_inviter"` // memberDoc.Body
ExpireAtUTC int64 `json:"_expire_at_utc"`
}
// 플레이어한테 공유하는 멤버 정보
type memberDoc struct {
Body bson.M `json:"_body"`
Invite bool `json:"_invite"`
InviteExpire int64 `json:"_invite_exp"`
}
type InvitationFail bson.M
type groupDoc struct {
Members map[string]any `json:"_members"`
InCharge string `json:"_incharge"`
Gid string `json:"_gid"`
rh *gocommon.RedisonHandler
id groupID
}
func (gd *groupDoc) loadMemberFull(tid string) (bson.M, error) {
full, err := gd.rh.JSONGet(gd.strid(), "$._members."+tid)
if err != nil {
return nil, err
}
bt := []byte(full.(string))
bt = bt[1 : len(bt)-1]
var doc bson.M
if err = json.Unmarshal(bt, &doc); err != nil {
return nil, err
}
return doc, nil
}
func (gd *groupDoc) loadFull() (doc bson.M) {
// 새 멤버에 그룹 전체를 알림
full, err := gd.rh.JSONGet(gd.strid(), "$")
if err == nil {
bt := []byte(full.(string))
bt = bt[1 : len(bt)-1]
err = json.Unmarshal(bt, &doc)
if err != nil {
logger.Println("loadFull err :", err)
}
} else {
logger.Println("loadFull err :", err)
}
return
}
func (gd *groupDoc) strid() string {
if len(gd.Gid) == 0 {
gd.Gid = gd.id.Hex()
}
return gd.Gid
}
func (gd *groupDoc) tid(in accountID) string {
return makeTid(gd.id, in)
}
func (gd *groupDoc) mid(tid string) accountID {
tidobj, _ := primitive.ObjectIDFromHex(tid)
var out primitive.ObjectID
for i := range tidobj {
out[12-i-1] = gd.id[i] ^ tidobj[12-i-1]
}
return out
}
func (gd *groupDoc) addInvite(mid accountID, body bson.M, ttl time.Duration, max int) (*memberDoc, error) {
targetmid := mid
targetbody := body
// 초대 가능한 빈 자리가 있나
tids, err := gd.rh.JSONObjKeys(gd.strid(), "$._members")
if err != nil {
return nil, err
}
now := time.Now().UTC()
createNewDoc := func() *memberDoc {
return &memberDoc{
Body: targetbody,
Invite: true,
InviteExpire: now.Add(ttl).Unix(),
}
}
newtid := gd.tid(targetmid)
if len(tids) < max {
// 빈자리를 찾았다.
newdoc := createNewDoc()
_, err := gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
return newdoc, err
}
expires, err := gd.rh.JSONGetInt64(gd.strid(), "$._members.._invite_exp")
if err != nil {
return nil, err
}
var delpaths []string
for i, expire := range expires {
if expire < now.Unix() {
// 만료된 초대가 있네? 지우자
delpaths = append(delpaths, "$._members."+tids[i])
}
}
if len(delpaths) == 0 {
// 빈자리가 없다
return nil, nil
}
if err := gd.rh.JSONMDel(gd.strid(), delpaths); err != nil {
return nil, err
}
newdoc := createNewDoc()
_, err = gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
return newdoc, err
}
func (gd *groupDoc) addMember(mid accountID, character bson.M) (bson.M, error) {
tid := gd.tid(mid)
prefix := "$._members." + tid
if _, err := gd.rh.JSONSet(gd.strid(), prefix+"._body", character, gocommon.RedisonSetOptionXX); err != nil {
return nil, err
}
if err := gd.rh.JSONMDel(gd.strid(), []string{prefix + "._invite", prefix + "._invite_exp"}); err != nil {
return nil, err
}
gd.rh.Persist(gd.rh.Context(), gd.strid()).Result()
return gd.loadMemberFull(tid)
}
func (gd *groupDoc) removeMemberByTid(tid string) error {
_, err := gd.rh.JSONDel(gd.strid(), "$._members."+tid)
if err != nil {
return err
}
counts, err := gd.rh.JSONObjLen(gd.strid(), "$._members")
if err != nil {
return err
}
if len(counts) > 0 && counts[0] == 0 {
_, err = gd.rh.Del(gd.rh.Context(), gd.strid()).Result()
}
return err
}
func (gd *groupDoc) removeMember(mid accountID) error {
return gd.removeMemberByTid(gd.tid(mid))
}
func (gd *groupDoc) getMembers() (map[string]any, error) {
res, err := gd.rh.JSONGet(gd.strid(), "$._members")
if err != nil {
return nil, err
}
var temp []map[string]any
err = json.Unmarshal([]byte(res.(string)), &temp)
if err != nil {
return nil, err
}
out := make(map[string]any)
for k, v := range temp[0] {
body := v.(map[string]any)["_body"]
out[gd.mid(k).Hex()] = body
}
return out, nil
}
type partyConfig struct {
InviteExpire int32 `json:"invite_ttl"` // 그룹이 개인에게 보낸 초대장 만료 기한
MaxMember int `json:"max_member"`
Name string
}
type groupParty struct {
partyConfig
sendUpstreamMessage func(*wshandler.UpstreamMessage)
enterRoom func(groupID, accountID)
leaveRoom func(groupID, accountID)
rh *gocommon.RedisonHandler
}
func (gp *groupParty) Initialize(tv *Tavern, cfg configDocument) error {
rem, _ := json.Marshal(cfg)
err := json.Unmarshal(rem, &gp.partyConfig)
if err != nil {
return err
}
gp.rh = tv.redison
gp.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) {
tv.wsh.SendUpstreamMessage(msg)
}
gp.enterRoom = func(gid groupID, accid accountID) {
tv.wsh.EnterRoom(gid.Hex(), accid)
}
gp.leaveRoom = func(gid groupID, accid accountID) {
tv.wsh.LeaveRoom(gid.Hex(), accid)
}
return nil
}
func (gp *groupParty) RegisterApiFunctions() {
}
// JoinParty : 그룹에 참가
// - type : 그룹 타입
// - 그룹 타입에 맞는 키(주로 _id)
// - member_id : 참가 멤버의 아이디
// - body : 멤버의 속성 bson document
func (gp *groupParty) JoinParty(w http.ResponseWriter, r *http.Request) {
var data struct {
Gid primitive.ObjectID `bson:"gid"`
Mid primitive.ObjectID `bson:"mid"`
Character bson.M `bson:"character"`
First bool `bson:"first"`
}
if err := gocommon.ReadJsonDocumentFromBody(r.Body, &data); err != nil {
logger.Println("JoinParty failed. ReadJsonDocumentFromBody returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
character := data.Character
gid := data.Gid
mid := data.Mid
if gid.IsZero() || mid.IsZero() {
logger.Println("JoinParty failed. mid should be exist")
w.WriteHeader(http.StatusBadRequest)
return
}
gd, err := gp.find(gid)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if gd == nil {
// 그룹이 없다. 실패
w.Write([]byte("{}"))
return
}
// 내 정보 업데이트할 때에도 사용됨
if data.First {
if memdoc, err := gd.addMember(mid, character); err == nil {
// 기존 유저에게 새 유저 알림
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: map[string]any{
gd.tid(mid): memdoc,
},
Tag: []string{"MemberDocFull"},
})
gp.enterRoom(gid, mid)
// 최초 입장이라면 새 멤버에 그룹 전체를 알림
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: mid.Hex(),
Body: gd.loadFull(),
Tag: []string{"GroupDocFull"},
})
enc := json.NewEncoder(w)
enc.Encode(map[string]string{
"gid": gid.Hex(),
"tid": gd.tid(mid),
})
} else if err != nil {
logger.Error("JoinParty failed :", err)
w.WriteHeader(http.StatusInternalServerError)
}
} else {
path := "$._members." + gd.tid(mid) + "._body"
if _, err := gd.rh.JSONSet(gd.strid(), path, character, gocommon.RedisonSetOptionXX); err != nil {
logger.Error("JoinParty failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 기존 유저에게 캐릭터 알림
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: map[string]any{
gd.tid(mid): bson.M{
"_body": character,
},
},
Tag: []string{"MemberDocFragment"},
})
}
}
// InviteToParty : 초대
// - type : 초대 타입 (required)
// - from : 초대하는 자 (required)
// - to : 초대받는 자 (required)
// - timeout : 초대 유지시간(optional. 없으면 config 기본 값)
// - (body) : 검색시 노출되는 document
func (gp *groupParty) InviteToParty(w http.ResponseWriter, r *http.Request) {
var doc struct {
Gid primitive.ObjectID `bson:"gid"`
Mid primitive.ObjectID `bson:"mid"`
Targetid primitive.ObjectID `bson:"targetid"`
Inviter bson.M `bson:"inviter"`
Invitee bson.M `bson:"invitee"`
}
if err := gocommon.ReadJsonDocumentFromBody(r.Body, &doc); err != nil {
logger.Println("InviteToParty failed. ReadJsonDocumentFromBody returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
targetid := doc.Targetid
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 {
gd, err = gp.createGroup(gid, mid, doc.Inviter)
if err != nil {
logger.Println("InviteToParty failed. gp.createGroup() return err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 내가 wshandler room에 입장
gp.enterRoom(gid, mid)
gp.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"},
})
w.Write([]byte(gd.strid()))
}
func (gp *groupParty) AcceptPartyInvitation(w http.ResponseWriter, r *http.Request) {
var doc struct {
Gid primitive.ObjectID `bson:"gid"`
Mid primitive.ObjectID `bson:"mid"`
Tid string `bson:"tid"`
Character bson.M `bson:"character"`
}
if err := gocommon.ReadJsonDocumentFromBody(r.Body, &doc); err != nil {
logger.Println("AcceptPartyInvitation failed. ReadJsonDocumentFromBody returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
gid := doc.Gid
mid := doc.Mid
member := doc.Character
cnt, err := gp.rh.Del(context.Background(), "inv."+mid.Hex()).Result()
if err != nil {
logger.Error("AcceptPartyInvitation failed. gp.rh.Del returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if cnt == 0 {
// 만료됨
w.Write([]byte("expired"))
return
}
pids, err := gp.rh.JSONGetString(mid.Hex(), "$.party.id")
if err != nil {
logger.Error("AcceptPartyInvitation failed. gp.rh.JSONGetString returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if len(pids) > 0 && len(pids[0]) > 0 {
// 기존에 이미 파티에 들어가 있다.
// 기존 파티에서는 탈퇴
oldgid, _ := primitive.ObjectIDFromHex(pids[0])
oldgd := &groupDoc{
id: oldgid,
rh: gp.rh,
}
// gid에는 제거 메시지 보냄
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + oldgd.strid(),
Body: bson.M{
oldgd.tid(mid): bson.M{},
},
Tag: []string{"MemberDocFull"},
})
gp.leaveRoom(oldgid, mid)
}
gd := &groupDoc{
id: gid,
rh: gp.rh,
}
memberDoc, err := gd.addMember(mid, member)
if err == nil {
// 기존 멤버에게 새 멤버를 알림
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: map[string]any{
gd.tid(mid): memberDoc,
},
Tag: []string{"MemberDocFull"},
})
gp.enterRoom(gid, mid)
// 현재 내 파티를 기록
gp.rh.JSONSet(mid.Hex(), "$.party", bson.M{"id": gid.Hex()})
// 새 멤버에 그룹 전체를 알림
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: mid.Hex(),
Body: gd.loadFull(),
Tag: []string{"GroupDocFull"},
})
} else {
logger.Println("AcceptPartyInvitation failed. group.AcceptPartyInvitation returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func (gp *groupParty) QueryPartyMemberState(w http.ResponseWriter, r *http.Request) {
var data struct {
Mid primitive.ObjectID `bson:"mid"`
}
if err := gocommon.ReadJsonDocumentFromBody(r.Body, &data); err != nil {
logger.Println("DenyPartyInvitation failed. ReadJsonDocumentFromBody returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
mid := data.Mid
states, err := gp.rh.JSONGetString(mid.Hex(), "$.party.state")
if err == redis.Nil {
return
}
if len(states) > 0 && len(states[0]) > 0 {
w.Write([]byte(states[0]))
} else {
w.Write([]byte("connected"))
}
}
func (gp *groupParty) updateMemberDocument(gid groupID, mid accountID, doc bson.M) error {
gd := &groupDoc{
id: gid,
rh: gp.rh,
}
prefixPath := fmt.Sprintf("$._members.%s.", gd.tid(mid))
err := gp.rh.JSONMSetRel(gd.strid(), prefixPath, doc)
if err != nil {
return err
}
if newstate, ok := doc["_state"]; ok {
gp.rh.JSONSet(mid.Hex(), "$.party.state", newstate)
}
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: map[string]any{
gd.tid(mid): doc,
},
Tag: []string{"MemberDocFragment"},
})
return nil
}
func (gp *groupParty) updatePartyDocument(gid groupID, frag bson.M) error {
gd := groupDoc{
id: gid,
rh: gp.rh,
}
if err := gp.rh.JSONMSetRel(gd.strid(), "$.", frag); err != nil {
return err
}
// 업데이트 알림
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gid.Hex(),
Body: frag,
Tag: []string{"GroupDocFragment"},
})
return nil
}
func (gp *groupParty) UpdatePartyDocument(w http.ResponseWriter, r *http.Request) {
var data struct {
Gid primitive.ObjectID `bson:"gid"`
Doc bson.M `bson:"doc"`
}
if err := gocommon.ReadJsonDocumentFromBody(r.Body, &data); err != nil {
logger.Println("UpdatePartyDocument failed. ReadJsonDocumentFromBody returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
gid := data.Gid
frag := data.Doc
if err := gp.updatePartyDocument(gid, frag); err != nil {
logger.Error("UpdatePartyDocument failed. group.UpdatePartyDocument returns err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
}
func (gp *groupParty) QueryPartyMembers(w http.ResponseWriter, r *http.Request) {
var data struct {
Gid primitive.ObjectID `bson:"gid"`
}
if err := gocommon.ReadJsonDocumentFromBody(r.Body, &data); err != nil {
logger.Println("QueryPartyMembers failed. ReadJsonDocumentFromBody returns err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
gid := data.Gid
gd := groupDoc{
id: gid,
rh: gp.rh,
}
members, err := gd.getMembers()
if err != nil {
logger.Error("QueryPartyMembers failed. group.QueryPartyMembers returns err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
enc := json.NewEncoder(w)
if err := enc.Encode(members); err != nil {
logger.Error("QueryPartyMembers failed. writeBsonDoc return err :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (gp *groupParty) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*groupDoc, error) {
tid := makeTid(newid, charge)
gd := &groupDoc{
Members: map[string]any{
tid: &memberDoc{
Body: chargeDoc,
Invite: false,
InviteExpire: 0,
},
},
InCharge: tid,
rh: gp.rh,
id: newid,
}
_, err := gp.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX)
if err != nil {
return nil, err
}
return gd, nil
}
func (gp *groupParty) find(id groupID) (*groupDoc, error) {
if id.IsZero() {
return nil, nil
}
_, err := gp.rh.JSONObjLen(id.Hex(), "$")
if err == redis.Nil {
return nil, nil
}
if err != nil {
return nil, err
}
return &groupDoc{
rh: gp.rh,
id: id,
}, nil
}
func (gp *groupParty) memberDisconnected(room string, mid primitive.ObjectID) {
gid, err := primitive.ObjectIDFromHex(room)
if err != nil {
return
}
gd := &groupDoc{
id: gid,
rh: gp.rh,
}
gd.removeMember(mid)
// 퇴장을 알림
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + room,
Body: bson.M{
gd.tid(mid): bson.M{},
},
Tag: []string{"MemberDocFull"},
})
}
func (gp *groupParty) ClientDisconnected(ctx wshandler.ApiCallContext) {
gids, _ := gp.rh.JSONGetString(ctx.CallBy.Accid.Hex(), "$.party.id")
if len(gids) > 0 && len(gids[0]) > 0 {
// mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐
gidstr := gids[0]
gid, _ := primitive.ObjectIDFromHex(gidstr)
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: ctx.CallBy.Accid.Hex(),
Body: bson.M{"gid": gid},
Tag: []string{"GroupDocFull", gidstr},
})
// gid에는 제거 메시지 보냄
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gidstr,
Body: bson.M{
makeTid(gid, ctx.CallBy.Accid): bson.M{},
},
Tag: []string{"MemberDocFull"},
})
gp.leaveRoom(gid, ctx.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 := groupDoc{
id: gid,
rh: gp.rh,
}
var err error
if len(tid) > 0 {
if tid != gd.tid(mid) {
// mid가 incharge여야 한다. 그래야 tid를 쫓아낼 수 있음
incharge, err := gp.rh.JSONGet(gd.strid(), "$._incharge")
if err != nil {
logger.Println("LeaveParty failed. gp.rh.JSONGet returns err :", err)
return
}
if !strings.Contains(incharge.(string), gd.tid(mid)) {
// incharge가 아니네?
logger.Println("LeaveParty failed. mid is not incharge")
return
}
mid = midFromTid(gd.id, tid)
}
err = gd.removeMemberByTid(tid)
} else {
err = gd.removeMember(mid)
// 내가 나갔다
gp.rh.JSONDel(mid.Hex(), "$.party.id")
}
if err != nil {
logger.Println("LeaveParty failed. gd.removeMember returns err :", err)
return
}
// mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: mid.Hex(),
Body: bson.M{"gid": gid},
Tag: []string{"GroupDocFull", gid.Hex()},
})
// gid에는 제거 메시지 보냄
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
Target: "#" + gd.strid(),
Body: bson.M{
tid: bson.M{},
},
Tag: []string{"MemberDocFull"},
})
gp.leaveRoom(gid, mid)
}
func (gp *groupParty) DenyPartyInvitation(ctx wshandler.ApiCallContext) {
gid, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string))
mid := ctx.CallBy.Accid
gp.rh.Del(context.Background(), "inv."+mid.Hex()).Result()
gd := groupDoc{
id: gid,
rh: gp.rh,
}
gd.removeMember(mid)
}

View File

@ -2,76 +2,28 @@ package core
import (
"context"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"reflect"
"strings"
"time"
"github.com/go-redis/redis/v8"
"repositories.action2quare.com/ayo/gocommon"
"repositories.action2quare.com/ayo/gocommon/flagx"
"repositories.action2quare.com/ayo/gocommon/logger"
"repositories.action2quare.com/ayo/gocommon/session"
"repositories.action2quare.com/ayo/gocommon/wshandler"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
defaultMaxMemory = 32 << 10 // 32 KB
)
func writeBsonArr(w io.Writer, src []bson.M) error {
return writeBsonDoc(w, bson.M{
"r": src,
})
}
func writeBsonDoc[T any](w io.Writer, src T) error {
rw, err := bsonrw.NewBSONValueWriter(w)
if err != nil {
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
}
var devflag = flagx.Bool("dev", false, "")
type TavernConfig struct {
gocommon.RegionStorageConfig `json:",inline"`
GroupTypes map[string]*groupConfig `json:"tavern_group_types"`
session.SessionConfig `json:",inline"`
Group map[string]configDocument `json:"tavern_group_types"`
MaingateApiToken string `json:"maingate_api_token"`
RedisURL string `json:"tavern_redis_url"`
macAddr string
@ -80,17 +32,10 @@ type TavernConfig struct {
var config TavernConfig
type Tavern struct {
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
redison *gocommon.RedisonHandler
httpApiBorker gocommon.HttpApiHandlerContainer
}
func getMacAddr() (string, error) {
@ -110,16 +55,11 @@ func getMacAddr() (string, error) {
}
// New :
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 {
func New(context context.Context, wsh *wshandler.WebsocketHandler) (*Tavern, error) {
if err := gocommon.LoadConfig(&config); err != nil {
return nil, err
}
inconfig = &loaded
}
config = *inconfig
macaddr, err := getMacAddr()
if err != nil {
return nil, err
@ -138,134 +78,92 @@ func New(context context.Context, wsh *wshandler.WebsocketHandler, inconfig *Tav
}
func (tv *Tavern) Cleanup() {
for _, st := range tv.subTaverns {
st.mongoClient.Close()
}
tv.mongoClient.Close()
}
func (tv *Tavern) prepare(ctx context.Context) error {
for region, addr := range config.RegionStorage {
var dbconn gocommon.MongoClient
var err error
var groupinstance group
var tmp *subTavern
methods := make(map[string]reflect.Method)
tp := reflect.TypeOf(tmp)
for i := 0; i < tp.NumMethod(); i++ {
method := tp.Method(i)
methods[method.Name] = method
}
redisClient, err := gocommon.NewRedisClient(addr.Redis["tavern"])
redisClient, err := gocommon.NewRedisClient(config.RedisURL)
if err != nil {
return err
}
sub := &subTavern{
wsh: tv.wsh,
mongoClient: dbconn,
redisClient: redisClient,
region: region,
methods: methods,
}
tv.redison = gocommon.NewRedisonHandler(redisClient.Context(), redisClient)
tv.wsh.RegisterApiHandler(wshandler.MakeWebsocketApiHandler(tv, "tv"))
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 {
if cfg, ok := config.Group["chat"]; ok {
chat := new(groupChat)
if err := chat.Initialize(tv, cfg); err != nil {
return err
}
groups[typename] = groupinstance
tv.httpApiBorker.RegisterApiHandler(gocommon.MakeHttpApiHandler(chat, "chat"))
tv.wsh.RegisterApiHandler(wshandler.MakeWebsocketApiHandler(chat, "chat"))
}
sub.groups = groups
tv.subTaverns = append(tv.subTaverns, sub)
if cfg, ok := config.Group["party"]; ok {
party := new(groupParty)
if err := party.Initialize(tv, cfg); err != nil {
return err
}
tv.httpApiBorker.RegisterApiHandler(gocommon.MakeHttpApiHandler(party, "party"))
tv.wsh.RegisterApiHandler(wshandler.MakeWebsocketApiHandler(party, "party"))
}
return nil
}
func (tv *Tavern) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux, prefix string) error {
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)
}
// tv.wsh.RegisterReceiver(tv)
pattern := gocommon.MakeHttpHandlerPattern(prefix, "api")
serveMux.HandleFunc(pattern, tv.api)
return nil
}
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) EnterChannel(ctx wshandler.ApiCallContext) {
tv.wsh.EnterRoom(ctx.Arguments[0].(string), ctx.CallBy.Accid)
}
func (sub *subTavern) OnRoomCreated(region, name string) {
_, err := sub.redisClient.Persist(context.Background(), name).Result()
func (tv *Tavern) LeaveChannel(ctx wshandler.ApiCallContext) {
tv.wsh.LeaveRoom(ctx.Arguments[0].(string), ctx.CallBy.Accid)
}
func (tv *Tavern) ClientConnected(ctx wshandler.ApiCallContext) {
logger.Println("ClientConnected :", ctx.CallBy.Alias)
tv.redison.Del(tv.redison.Context(), ctx.CallBy.Accid.Hex())
_, err := tv.redison.JSONSet(ctx.CallBy.Accid.Hex(), "$", bson.M{"_ts": time.Now().UTC().Unix()})
if err != nil {
logger.Println("OnRoomCreate Persist failed :", err)
logger.Println("OnClientMessageReceived HSet error :", err)
}
}
func (sub *subTavern) OnRoomDestroyed(region, name string) {
_, err := sub.redisClient.Expire(context.Background(), name, 3600*time.Second).Result()
func (tv *Tavern) ClientDisconnected(ctx wshandler.ApiCallContext) {
tv.redison.Del(tv.redison.Context(), ctx.CallBy.Accid.Hex()).Result()
logger.Println("ClientDisconnected :", ctx.CallBy.Alias)
}
func (tv *Tavern) OnRoomCreated(name string) {
cnt, err := tv.redison.IncrBy(tv.redison.Context(), "_ref_"+name, 1).Result()
if err != nil && !errors.Is(err, redis.Nil) {
logger.Println("OnRoomCreated JSONSet failed :", err)
return
}
if cnt == 1 {
tv.redison.JSONSet(name, "$", map[string]any{}, gocommon.RedisonSetOptionNX)
}
}
func (tv *Tavern) OnRoomDestroyed(name string) {
cnt, err := tv.redison.IncrBy(tv.redison.Context(), "_ref_"+name, -1).Result()
if err != nil {
logger.Println("OnRoomDestroyed Persist failed :", err)
logger.Println("OnRoomDestroyed JSONNumIncrBy failed :", err)
} else if cnt == 0 {
tv.redison.Del(tv.redison.Context(), "_ref_"+name)
tv.redison.JSONDel(name, "$")
}
}
func (sub *subTavern) api(w http.ResponseWriter, r *http.Request) {
func (tv *Tavern) api(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
@ -284,29 +182,12 @@ func (sub *subTavern) api(w http.ResponseWriter, r *http.Request) {
return
}
operation := r.URL.Query().Get("operation")
if len(operation) == 0 {
funcname := r.URL.Query().Get("call")
if len(funcname) == 0 {
logger.Println("query param 'call' is missing")
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)
tv.httpApiBorker.Call(funcname, w, r)
}

View File

@ -3,6 +3,7 @@ package core
import (
"context"
"encoding/binary"
"fmt"
"testing"
"time"
@ -30,11 +31,46 @@ 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{
@ -64,7 +100,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",
})

2
go.mod
View File

@ -5,7 +5,7 @@ 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-20230719003337-29b2f258507d
repositories.action2quare.com/ayo/gocommon v0.0.0-20230908062630-46ce5f09897a
)
require (

36
go.sum
View File

@ -104,31 +104,11 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
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-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=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230906142024-eb54fa2e3a44 h1:90XY5WSLtxvfi6YktDY4Sv1CMPRViZvPLPunA1eIxZA=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230906142024-eb54fa2e3a44/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230908023557-6cbf32c3868b h1:Rx6tP6IhlGlVGGgMDZ7OuIDU9cHfvm2L05L2tqF7G58=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230908023557-6cbf32c3868b/go.mod h1:XvklTTSvQX5uviivGBcZo8eIL+mV94W2e4uBBXcT5JY=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230908025007-3603c0386b29 h1:Ts40m9MLMMx4uaQWko5QXkg/HX4uYQB9TGGEN6twhiU=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230908025007-3603c0386b29/go.mod h1:XvklTTSvQX5uviivGBcZo8eIL+mV94W2e4uBBXcT5JY=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230908062630-46ce5f09897a h1:xKUI2xlP6LcUV5fy+4QEHoaZOhkSsMYgeIp6H5ADBCM=
repositories.action2quare.com/ayo/gocommon v0.0.0-20230908062630-46ce5f09897a/go.mod h1:XvklTTSvQX5uviivGBcZo8eIL+mV94W2e4uBBXcT5JY=

16
main.go
View File

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