party로 그룹 변경
This commit is contained in:
@ -13,11 +13,11 @@
|
||||
"dev": {
|
||||
"mongo": "mongodb://192.168.8.94:27017/?replicaSet=repl01&retrywrites=false",
|
||||
"redis": {
|
||||
"cache": "redis://192.168.8.94:6380/4",
|
||||
"session": "redis://192.168.8.94:6380/5",
|
||||
"ranking": "redis://192.168.8.94:6380/6",
|
||||
"wshandler": "redis://192.168.8.94:6380/7",
|
||||
"tavern": "redis://192.168.8.94:6380/8"
|
||||
"cache": "redis://192.168.8.94:6380/5",
|
||||
"session": "redis://192.168.8.94:6380/6",
|
||||
"ranking": "redis://192.168.8.94:6380/7",
|
||||
"wshandler": "redis://192.168.8.94:6380/8",
|
||||
"tavern": "redis://192.168.8.94:6380/9"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -26,48 +26,12 @@
|
||||
"maingate_api_token": "63d08aa34f0162622c11284b",
|
||||
"tavern_service_url": "http://localhost/tavern",
|
||||
"tavern_group_types": {
|
||||
"subjugate": {
|
||||
"text_search_field": [
|
||||
"name"
|
||||
],
|
||||
"unique_index": [
|
||||
"name,_id",
|
||||
"_id,members",
|
||||
"name,hidden"
|
||||
],
|
||||
"search_index": [
|
||||
"rules"
|
||||
],
|
||||
"member_index": [
|
||||
"_gid,candidate,luts",
|
||||
"_gid,luts",
|
||||
"_gid,expiring"
|
||||
],
|
||||
"invite_ttl": 30,
|
||||
"candidate_ttl": 3600,
|
||||
"invitee_exlusive": true,
|
||||
"invitee_is_member": true,
|
||||
"max_member": 4
|
||||
},
|
||||
"lobby": {
|
||||
"party": {
|
||||
"max_member": 3,
|
||||
"invitee_exlusive": true,
|
||||
"invitee_is_member": true,
|
||||
"transient": true,
|
||||
"invite_ttl": 30
|
||||
}
|
||||
},
|
||||
|
||||
"services": {
|
||||
"kingdom": {
|
||||
"개발중": {
|
||||
"url": "http://localhost/warehouse/dev",
|
||||
"development": true
|
||||
},
|
||||
"개인서버": {
|
||||
"url": "http://localhost/warehouse/private",
|
||||
"development": false
|
||||
}
|
||||
"chat" : {
|
||||
"transient" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
526
core/apiimpl.go
526
core/apiimpl.go
@ -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
|
||||
}
|
||||
}
|
||||
@ -1,43 +1,55 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
)
|
||||
|
||||
type groupConfig struct {
|
||||
UniqueIndex []string `json:"unique_index"`
|
||||
SearchIndex []string `json:"search_index"`
|
||||
MemberIndex []string `json:"member_index"`
|
||||
TextSearchFields []string `json:"text_search_field"`
|
||||
InviteExpire int32 `json:"invite_ttl"` // 그룹이 개인에게 보낸 초대장 만료 기한
|
||||
CandidateExpire int32 `json:"candidate_ttl"` // 개인이 그룹에게 보낸 신청서 만료 기한
|
||||
InviteeExlusive bool `json:"invitee_exlusive"`
|
||||
InviteeIsMember bool `json:"invitee_is_member"`
|
||||
MaxMember int `json:"max_member"`
|
||||
Transient bool `json:"transient"`
|
||||
var groupTypes map[string]reflect.Type
|
||||
|
||||
Name string
|
||||
func groupTypeContainer() map[string]reflect.Type {
|
||||
if groupTypes == nil {
|
||||
groupTypes = make(map[string]reflect.Type)
|
||||
}
|
||||
return groupTypes
|
||||
}
|
||||
|
||||
type apiFuncType func(http.ResponseWriter, *http.Request)
|
||||
type apiFuncsContainer struct {
|
||||
normfuncs map[string]apiFuncType
|
||||
funcs map[string][]apiFuncType
|
||||
}
|
||||
|
||||
func (afc *apiFuncsContainer) registApiFunction(name string, f apiFuncType) {
|
||||
afc.funcs[name] = append(afc.funcs[name], f)
|
||||
}
|
||||
|
||||
func (afc *apiFuncsContainer) normalize() {
|
||||
for k, v := range afc.funcs {
|
||||
if len(v) == 1 {
|
||||
afc.normfuncs[k] = v[0]
|
||||
} else {
|
||||
afc.normfuncs[k] = func(w http.ResponseWriter, r *http.Request) {
|
||||
for _, f := range v {
|
||||
f(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
afc.funcs = nil
|
||||
}
|
||||
|
||||
func (afc *apiFuncsContainer) call(fn string, w http.ResponseWriter, r *http.Request) {
|
||||
f := afc.normfuncs[fn]
|
||||
if f != nil {
|
||||
f(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
type configDocument map[string]any
|
||||
type group interface {
|
||||
Create(form url.Values, doc bson.M) (primitive.ObjectID, error)
|
||||
Candidate(groupID primitive.ObjectID, memberID primitive.ObjectID, doc bson.M) error
|
||||
Join(groupID primitive.ObjectID, memberID primitive.ObjectID, doc bson.M) error
|
||||
Invite(groupID primitive.ObjectID, memberID primitive.ObjectID, inviterDoc bson.M, inviteeDoc bson.M) (string, error)
|
||||
CancelInvitation(groupID primitive.ObjectID, ticketID primitive.ObjectID) error
|
||||
AcceptInvitation(groupID primitive.ObjectID, mid primitive.ObjectID, member bson.M) error
|
||||
DenyInvitation(groupID primitive.ObjectID, mid primitive.ObjectID, ticketID primitive.ObjectID) error
|
||||
QueryInvitations(memberID primitive.ObjectID, after primitive.Timestamp) ([]bson.M, error)
|
||||
Exist(groupID primitive.ObjectID, filter bson.M) (bool, error)
|
||||
FindAll(filter bson.M, projection string, after primitive.Timestamp) ([]bson.M, error)
|
||||
FindOne(groupID primitive.ObjectID, projection string) (bson.M, error)
|
||||
Leave(groupID primitive.ObjectID, memberID primitive.ObjectID) error
|
||||
UpdateMemberDocument(groupID primitive.ObjectID, memberID primitive.ObjectID, doc bson.M) error
|
||||
Dismiss(groupID primitive.ObjectID) error
|
||||
UpdateGroupDocument(groupID primitive.ObjectID, doc bson.M) error
|
||||
QueryGroupMembers(groupID primitive.ObjectID) (bson.M, error)
|
||||
MemberDisconnected(room string, mid primitive.ObjectID)
|
||||
Initialize(*subTavern, configDocument) error
|
||||
ClientMessageReceved(*wshandler.Sender, wshandler.WebSocketMessageType, any)
|
||||
}
|
||||
|
||||
22
core/group_chat.go
Normal file
22
core/group_chat.go
Normal file
@ -0,0 +1,22 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
)
|
||||
|
||||
func init() {
|
||||
groupTypeContainer()["chat"] = reflect.TypeOf(&groupChat{})
|
||||
}
|
||||
|
||||
type groupChat struct {
|
||||
}
|
||||
|
||||
func (gc *groupChat) Initialize(*subTavern, configDocument) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gc *groupChat) ClientMessageReceved(*wshandler.Sender, wshandler.WebSocketMessageType, any) {
|
||||
|
||||
}
|
||||
@ -1,577 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"repositories.action2quare.com/ayo/gocommon"
|
||||
"repositories.action2quare.com/ayo/gocommon/logger"
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type accountID = primitive.ObjectID
|
||||
type ticketID = primitive.ObjectID
|
||||
type groupID = primitive.ObjectID
|
||||
|
||||
func init() {
|
||||
gob.Register(memberDoc{})
|
||||
gob.Register(groupDoc{})
|
||||
gob.Register(Invitation{})
|
||||
gob.Register(InvitationFail{})
|
||||
}
|
||||
|
||||
func makeTid(gid groupID, in accountID) string {
|
||||
var out primitive.ObjectID
|
||||
for i := range in {
|
||||
out[12-i-1] = gid[i] ^ in[12-i-1]
|
||||
}
|
||||
return out.Hex()
|
||||
}
|
||||
|
||||
type Invitation struct {
|
||||
GroupID groupID `json:"_gid"`
|
||||
TicketID string `json:"_tid"`
|
||||
Inviter bson.M `json:"_inviter"` // memberDoc.Body
|
||||
ExpireAtUTC int64 `json:"_expire_at_utc"`
|
||||
}
|
||||
|
||||
// 플레이어한테 공유하는 멤버 정보
|
||||
type memberDoc struct {
|
||||
Body bson.M `json:"_body"`
|
||||
Invite bool `json:"_invite"`
|
||||
InviteExpire int64 `json:"_invite_exp"`
|
||||
}
|
||||
|
||||
type InvitationFail bson.M
|
||||
|
||||
type groupDoc struct {
|
||||
Members map[string]any `json:"_members"`
|
||||
InCharge string `json:"_incharge"`
|
||||
Gid string `json:"_gid"`
|
||||
|
||||
rh *gocommon.RedisonHandler
|
||||
id groupID
|
||||
}
|
||||
|
||||
func (gd *groupDoc) loadMemberFull(tid string) (bson.M, error) {
|
||||
full, err := gd.rh.JSONGet(gd.strid(), "$._members."+tid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bt := []byte(full.(string))
|
||||
bt = bt[1 : len(bt)-1]
|
||||
|
||||
var doc bson.M
|
||||
if err = json.Unmarshal(bt, &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func (gd *groupDoc) loadFull() (doc bson.M) {
|
||||
// 새 멤버에 그룹 전체를 알림
|
||||
full, err := gd.rh.JSONGet(gd.strid(), "$")
|
||||
if err == nil {
|
||||
bt := []byte(full.(string))
|
||||
bt = bt[1 : len(bt)-1]
|
||||
err = json.Unmarshal(bt, &doc)
|
||||
if err != nil {
|
||||
logger.Println("loadFull err :", err)
|
||||
}
|
||||
} else {
|
||||
logger.Println("loadFull err :", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (gd *groupDoc) strid() string {
|
||||
if len(gd.Gid) == 0 {
|
||||
gd.Gid = gd.id.Hex()
|
||||
}
|
||||
return gd.Gid
|
||||
}
|
||||
|
||||
func (gd *groupDoc) tid(in accountID) string {
|
||||
return makeTid(gd.id, in)
|
||||
}
|
||||
|
||||
func (gd *groupDoc) mid(tid string) accountID {
|
||||
tidobj, _ := primitive.ObjectIDFromHex(tid)
|
||||
var out primitive.ObjectID
|
||||
for i := range tidobj {
|
||||
out[12-i-1] = gd.id[i] ^ tidobj[12-i-1]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (gd *groupDoc) addInvite(inviteeDoc bson.M, ttl time.Duration, max int) (*memberDoc, error) {
|
||||
targetmid := inviteeDoc["_mid"].(accountID)
|
||||
targetbody := inviteeDoc["body"].(bson.M)
|
||||
|
||||
// 초대 가능한 빈 자리가 있나
|
||||
tids, err := gd.rh.JSONObjKeys(gd.strid(), "$._members")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
createNewDoc := func() *memberDoc {
|
||||
return &memberDoc{
|
||||
Body: targetbody,
|
||||
Invite: true,
|
||||
InviteExpire: now.Add(ttl).Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
newtid := gd.tid(targetmid)
|
||||
if len(tids) < max {
|
||||
// 빈자리를 찾았다.
|
||||
newdoc := createNewDoc()
|
||||
_, err := gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
|
||||
return newdoc, err
|
||||
}
|
||||
|
||||
expires, err := gd.rh.JSONGetInt64(gd.strid(), "$._members.._invite_exp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var delpaths []string
|
||||
for i, expire := range expires {
|
||||
if expire < now.Unix() {
|
||||
// 만료된 초대가 있네? 지우자
|
||||
delpaths = append(delpaths, "$._members."+tids[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(delpaths) == 0 {
|
||||
// 빈자리가 없다
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := gd.rh.JSONMDel(gd.strid(), delpaths); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newdoc := createNewDoc()
|
||||
_, err = gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
|
||||
return newdoc, err
|
||||
}
|
||||
|
||||
func (gd *groupDoc) addMember(mid accountID, doc bson.M) (bson.M, error) {
|
||||
tid := gd.tid(mid)
|
||||
prefix := "$._members." + tid
|
||||
|
||||
if _, err := gd.rh.JSONMerge(gd.strid(), prefix+"._body", doc, gocommon.RedisonSetOptionXX); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := gd.rh.JSONMDel(gd.strid(), []string{prefix + "._invite", prefix + "._invite_exp"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gd.loadMemberFull(tid)
|
||||
}
|
||||
|
||||
func (gd *groupDoc) removeMember(mid accountID) error {
|
||||
_, err := gd.rh.JSONDel(gd.strid(), "$._members."+gd.tid(mid))
|
||||
return err
|
||||
}
|
||||
|
||||
func (gd *groupDoc) getMembers() (map[string]any, error) {
|
||||
res, err := gd.rh.JSONGet(gd.strid(), "$._members")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var temp []map[string]any
|
||||
err = json.Unmarshal([]byte(res.(string)), &temp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(map[string]any)
|
||||
for k, v := range temp[0] {
|
||||
body := v.(map[string]any)["_body"]
|
||||
out[gd.mid(k).Hex()] = body
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type groupInMemory struct {
|
||||
*groupConfig
|
||||
sendUpstreamMessage func(*wshandler.UpstreamMessage)
|
||||
sendEnterRoomMessage func(groupID, accountID)
|
||||
sendLeaveRoomMessage func(groupID, accountID)
|
||||
rh *gocommon.RedisonHandler
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*groupDoc, error) {
|
||||
tid := makeTid(newid, charge)
|
||||
|
||||
gd := &groupDoc{
|
||||
Members: map[string]any{
|
||||
tid: &memberDoc{
|
||||
Body: chargeDoc,
|
||||
Invite: false,
|
||||
InviteExpire: 0,
|
||||
},
|
||||
},
|
||||
InCharge: tid,
|
||||
|
||||
rh: gm.rh,
|
||||
id: newid,
|
||||
}
|
||||
|
||||
_, err := gm.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gd, nil
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) find(id groupID) (*groupDoc, error) {
|
||||
if id.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, err := gm.rh.JSONObjLen(id.Hex(), "$")
|
||||
if err == redis.Nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &groupDoc{
|
||||
rh: gm.rh,
|
||||
id: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) Create(form url.Values, base bson.M) (groupID, error) {
|
||||
return primitive.NilObjectID, nil
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) Candidate(gid groupID, mid accountID, doc bson.M) error {
|
||||
logger.Error("not implemented func : Canidate")
|
||||
return nil
|
||||
}
|
||||
|
||||
var errGroupNotExist = errors.New("group does not exist")
|
||||
|
||||
func (gm *groupInMemory) Join(gid groupID, mid accountID, doc bson.M) error {
|
||||
gd, err := gm.find(gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gd == nil {
|
||||
// 그룹이 없다. 실패
|
||||
return errGroupNotExist
|
||||
}
|
||||
|
||||
// 내 정보 업데이트할 때에도 사용됨
|
||||
if memdoc, err := gd.addMember(mid, doc); err == nil {
|
||||
// 기존 유저에게 새 유저 알림
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: map[string]any{
|
||||
gd.tid(mid): memdoc,
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
|
||||
gm.sendEnterRoomMessage(gid, mid)
|
||||
|
||||
// 새 멤버에 그룹 전체를 알림
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: gd.loadFull(),
|
||||
Tag: []string{"GroupDocFull"},
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var errInviteeDocMidMissing = errors.New("inviteeDoc must have '_mid' field")
|
||||
var errAlreadyInvited = errors.New("this target is already invited by someone or me")
|
||||
|
||||
func (gm *groupInMemory) Invite(gid groupID, mid accountID, inviterDoc bson.M, inviteeDoc bson.M) (string, error) {
|
||||
targetid, ok := inviteeDoc["_mid"].(accountID)
|
||||
if !ok {
|
||||
return "", errInviteeDocMidMissing
|
||||
}
|
||||
|
||||
// targetid에 초대한 mid가 들어있다.
|
||||
already, err := gm.rh.Get(context.Background(), targetid.Hex()).Result()
|
||||
if err != nil && err != redis.Nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(already) > 0 {
|
||||
if already != mid.Hex() {
|
||||
// 이미 초대 중이다.
|
||||
// inviter한테 알려줘야 한다.
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: inviteeDoc,
|
||||
Tag: []string{"InvitationFail"},
|
||||
})
|
||||
}
|
||||
return "", errAlreadyInvited
|
||||
}
|
||||
|
||||
gd, err := gm.find(gid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if gd == nil {
|
||||
gd, err = gm.createGroup(gid, mid, inviterDoc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 내가 wshandler room에 입장
|
||||
gm.sendEnterRoomMessage(gid, mid)
|
||||
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: gd,
|
||||
Tag: []string{"GroupDocFull"},
|
||||
})
|
||||
}
|
||||
|
||||
newdoc, err := gd.addInvite(inviteeDoc, time.Duration(gm.InviteExpire+1)*time.Second, gm.MaxMember)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 초대 중 표시
|
||||
_, err = gm.rh.SetNX(context.Background(), targetid.Hex(), mid.Hex(), time.Duration(gm.InviteExpire)*time.Second).Result()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// invitee에게 알림
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + targetid.Hex(),
|
||||
Body: Invitation{
|
||||
GroupID: gid,
|
||||
TicketID: gd.tid(targetid),
|
||||
Inviter: inviterDoc,
|
||||
ExpireAtUTC: newdoc.InviteExpire,
|
||||
},
|
||||
Tag: []string{"Invitation"},
|
||||
})
|
||||
|
||||
return gd.strid(), nil
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) CancelInvitation(gid groupID, tid ticketID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errInvitationExpired = errors.New("invitation is already expired")
|
||||
|
||||
func (gm *groupInMemory) AcceptInvitation(gid groupID, mid accountID, member bson.M) error {
|
||||
cnt, err := gm.rh.Del(context.Background(), mid.Hex()).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt == 0 {
|
||||
// 만료됨
|
||||
return errInvitationExpired
|
||||
}
|
||||
|
||||
gd := &groupDoc{
|
||||
id: gid,
|
||||
rh: gm.rh,
|
||||
}
|
||||
|
||||
memberDoc, err := gd.addMember(mid, member)
|
||||
if err == nil {
|
||||
// 기존 멤버에게 새 멤버를 알림
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: map[string]any{
|
||||
gd.tid(mid): memberDoc,
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
|
||||
gm.sendEnterRoomMessage(gid, mid)
|
||||
|
||||
// 새 멤버에 그룹 전체를 알림
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: gd.loadFull(),
|
||||
Tag: []string{"GroupDocFull"},
|
||||
})
|
||||
}
|
||||
|
||||
// 실패
|
||||
return err
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) QueryGroupMembers(gid groupID) (bson.M, error) {
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gm.rh,
|
||||
}
|
||||
return gd.getMembers()
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) DenyInvitation(gid groupID, mid accountID, tid ticketID) error {
|
||||
gm.rh.Del(context.Background(), mid.Hex()).Result()
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gm.rh,
|
||||
}
|
||||
return gd.removeMember(mid)
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) QueryInvitations(mid accountID, after primitive.Timestamp) ([]bson.M, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (gm *groupInMemory) Exist(gid groupID, filter bson.M) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (gm *groupInMemory) FindAll(filter bson.M, projection string, after primitive.Timestamp) ([]bson.M, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (gm *groupInMemory) FindOne(gid groupID, projection string) (bson.M, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (gm *groupInMemory) Leave(gid groupID, mid accountID) error {
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gm.rh,
|
||||
}
|
||||
if err := gd.removeMember(mid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 나한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: bson.M{"gid": gid},
|
||||
Tag: []string{"GroupDocFull", gid.Hex()},
|
||||
})
|
||||
gm.sendLeaveRoomMessage(gid, mid)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) UpdateMemberDocument(gid groupID, mid accountID, doc bson.M) error {
|
||||
gd := &groupDoc{
|
||||
id: gid,
|
||||
rh: gm.rh,
|
||||
}
|
||||
|
||||
prefixPath := fmt.Sprintf("$._members.%s.", gd.tid(mid))
|
||||
err := gm.rh.JSONMSetRel(gd.strid(), prefixPath, doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: map[string]any{
|
||||
gd.tid(mid): doc,
|
||||
},
|
||||
Tag: []string{"MemberDocFragment"},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) Dismiss(gid groupID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) UpdateGroupDocument(gid groupID, frag bson.M) error {
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gm.rh,
|
||||
}
|
||||
|
||||
if err := gm.rh.JSONMSetRel(gd.strid(), "$.", frag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 업데이트 알림
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: frag,
|
||||
Tag: []string{"GroupDocFragment"},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gm *groupInMemory) MemberDisconnected(room string, mid primitive.ObjectID) {
|
||||
gid, err := primitive.ObjectIDFromHex(room)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
gd := &groupDoc{
|
||||
id: gid,
|
||||
rh: gm.rh,
|
||||
}
|
||||
|
||||
tid := gd.tid(mid)
|
||||
deleted, _ := gm.rh.JSONDel(room, "$._members."+tid)
|
||||
if deleted > 0 {
|
||||
// 퇴장을 알림
|
||||
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + room,
|
||||
Body: bson.M{
|
||||
tid: bson.M{},
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *groupConfig) prepareInMemory(ctx context.Context, typename string, sub *subTavern) (group, error) {
|
||||
// group document
|
||||
// member document
|
||||
region := sub.region
|
||||
wsh := sub.wsh
|
||||
storage := config.RegionStorage[sub.region]
|
||||
redisClient, err := gocommon.NewRedisClient(storage.Redis["tavern"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 여기서는 subscribe channel
|
||||
// 각 함수에서는 publish
|
||||
gm := &groupInMemory{
|
||||
groupConfig: cfg,
|
||||
rh: gocommon.NewRedisonHandler(ctx, redisClient),
|
||||
sendUpstreamMessage: func(msg *wshandler.UpstreamMessage) {
|
||||
wsh.SendUpstreamMessage(region, msg)
|
||||
},
|
||||
sendEnterRoomMessage: func(gid groupID, accid accountID) {
|
||||
wsh.EnterRoom(region, gid.Hex(), accid)
|
||||
},
|
||||
sendLeaveRoomMessage: func(gid groupID, accid accountID) {
|
||||
wsh.LeaveRoom(region, gid.Hex(), accid)
|
||||
},
|
||||
}
|
||||
|
||||
return gm, nil
|
||||
}
|
||||
759
core/group_party.go
Normal file
759
core/group_party.go
Normal file
@ -0,0 +1,759 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"repositories.action2quare.com/ayo/gocommon"
|
||||
"repositories.action2quare.com/ayo/gocommon/logger"
|
||||
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type accountID = primitive.ObjectID
|
||||
type groupID = primitive.ObjectID
|
||||
|
||||
func init() {
|
||||
gob.Register(memberDoc{})
|
||||
gob.Register(groupDoc{})
|
||||
gob.Register(Invitation{})
|
||||
gob.Register(InvitationFail{})
|
||||
|
||||
groupTypeContainer()["party"] = reflect.TypeOf(&groupParty{})
|
||||
}
|
||||
|
||||
func makeTid(gid groupID, in accountID) string {
|
||||
var out primitive.ObjectID
|
||||
for i := range in {
|
||||
out[12-i-1] = gid[i] ^ in[12-i-1]
|
||||
}
|
||||
return out.Hex()
|
||||
}
|
||||
|
||||
type Invitation struct {
|
||||
GroupID groupID `json:"_gid"`
|
||||
TicketID string `json:"_tid"`
|
||||
Inviter bson.M `json:"_inviter"` // memberDoc.Body
|
||||
ExpireAtUTC int64 `json:"_expire_at_utc"`
|
||||
}
|
||||
|
||||
// 플레이어한테 공유하는 멤버 정보
|
||||
type memberDoc struct {
|
||||
Body bson.M `json:"_body"`
|
||||
Invite bool `json:"_invite"`
|
||||
InviteExpire int64 `json:"_invite_exp"`
|
||||
}
|
||||
|
||||
type InvitationFail bson.M
|
||||
|
||||
type groupDoc struct {
|
||||
Members map[string]any `json:"_members"`
|
||||
InCharge string `json:"_incharge"`
|
||||
Gid string `json:"_gid"`
|
||||
|
||||
rh *gocommon.RedisonHandler
|
||||
id groupID
|
||||
}
|
||||
|
||||
func (gd *groupDoc) loadMemberFull(tid string) (bson.M, error) {
|
||||
full, err := gd.rh.JSONGet(gd.strid(), "$._members."+tid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bt := []byte(full.(string))
|
||||
bt = bt[1 : len(bt)-1]
|
||||
|
||||
var doc bson.M
|
||||
if err = json.Unmarshal(bt, &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func (gd *groupDoc) loadFull() (doc bson.M) {
|
||||
// 새 멤버에 그룹 전체를 알림
|
||||
full, err := gd.rh.JSONGet(gd.strid(), "$")
|
||||
if err == nil {
|
||||
bt := []byte(full.(string))
|
||||
bt = bt[1 : len(bt)-1]
|
||||
err = json.Unmarshal(bt, &doc)
|
||||
if err != nil {
|
||||
logger.Println("loadFull err :", err)
|
||||
}
|
||||
} else {
|
||||
logger.Println("loadFull err :", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (gd *groupDoc) strid() string {
|
||||
if len(gd.Gid) == 0 {
|
||||
gd.Gid = gd.id.Hex()
|
||||
}
|
||||
return gd.Gid
|
||||
}
|
||||
|
||||
func (gd *groupDoc) tid(in accountID) string {
|
||||
return makeTid(gd.id, in)
|
||||
}
|
||||
|
||||
func (gd *groupDoc) mid(tid string) accountID {
|
||||
tidobj, _ := primitive.ObjectIDFromHex(tid)
|
||||
var out primitive.ObjectID
|
||||
for i := range tidobj {
|
||||
out[12-i-1] = gd.id[i] ^ tidobj[12-i-1]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (gd *groupDoc) addInvite(inviteeDoc bson.M, ttl time.Duration, max int) (*memberDoc, error) {
|
||||
targetmid := inviteeDoc["_mid"].(accountID)
|
||||
targetbody := inviteeDoc["body"].(bson.M)
|
||||
|
||||
// 초대 가능한 빈 자리가 있나
|
||||
tids, err := gd.rh.JSONObjKeys(gd.strid(), "$._members")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
createNewDoc := func() *memberDoc {
|
||||
return &memberDoc{
|
||||
Body: targetbody,
|
||||
Invite: true,
|
||||
InviteExpire: now.Add(ttl).Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
newtid := gd.tid(targetmid)
|
||||
if len(tids) < max {
|
||||
// 빈자리를 찾았다.
|
||||
newdoc := createNewDoc()
|
||||
_, err := gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
|
||||
return newdoc, err
|
||||
}
|
||||
|
||||
expires, err := gd.rh.JSONGetInt64(gd.strid(), "$._members.._invite_exp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var delpaths []string
|
||||
for i, expire := range expires {
|
||||
if expire < now.Unix() {
|
||||
// 만료된 초대가 있네? 지우자
|
||||
delpaths = append(delpaths, "$._members."+tids[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(delpaths) == 0 {
|
||||
// 빈자리가 없다
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := gd.rh.JSONMDel(gd.strid(), delpaths); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newdoc := createNewDoc()
|
||||
_, err = gd.rh.JSONSet(gd.strid(), "$._members."+newtid, newdoc)
|
||||
return newdoc, err
|
||||
}
|
||||
|
||||
func (gd *groupDoc) addMember(mid accountID, doc bson.M) (bson.M, error) {
|
||||
tid := gd.tid(mid)
|
||||
prefix := "$._members." + tid
|
||||
|
||||
if _, err := gd.rh.JSONMerge(gd.strid(), prefix+"._body", doc, gocommon.RedisonSetOptionXX); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := gd.rh.JSONMDel(gd.strid(), []string{prefix + "._invite", prefix + "._invite_exp"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gd.loadMemberFull(tid)
|
||||
}
|
||||
|
||||
func (gd *groupDoc) removeMember(mid accountID) error {
|
||||
_, err := gd.rh.JSONDel(gd.strid(), "$._members."+gd.tid(mid))
|
||||
return err
|
||||
}
|
||||
|
||||
func (gd *groupDoc) getMembers() (map[string]any, error) {
|
||||
res, err := gd.rh.JSONGet(gd.strid(), "$._members")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var temp []map[string]any
|
||||
err = json.Unmarshal([]byte(res.(string)), &temp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(map[string]any)
|
||||
for k, v := range temp[0] {
|
||||
body := v.(map[string]any)["_body"]
|
||||
out[gd.mid(k).Hex()] = body
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type partyConfig struct {
|
||||
InviteExpire int32 `json:"invite_ttl"` // 그룹이 개인에게 보낸 초대장 만료 기한
|
||||
MaxMember int `json:"max_member"`
|
||||
|
||||
Name string
|
||||
}
|
||||
|
||||
type groupParty struct {
|
||||
partyConfig
|
||||
sendUpstreamMessage func(*wshandler.UpstreamMessage)
|
||||
sendEnterRoomMessage func(groupID, accountID)
|
||||
sendLeaveRoomMessage func(groupID, accountID)
|
||||
rh *gocommon.RedisonHandler
|
||||
}
|
||||
|
||||
func (gp *groupParty) Initialize(sub *subTavern, cfg configDocument) error {
|
||||
rem, _ := json.Marshal(cfg)
|
||||
err := json.Unmarshal(rem, &gp.partyConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gp.rh = gocommon.NewRedisonHandler(sub.redisClient.Context(), sub.redisClient)
|
||||
gp.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) {
|
||||
sub.wsh.SendUpstreamMessage(sub.region, msg)
|
||||
}
|
||||
gp.sendEnterRoomMessage = func(gid groupID, accid accountID) {
|
||||
sub.wsh.EnterRoom(sub.region, gid.Hex(), accid)
|
||||
}
|
||||
gp.sendLeaveRoomMessage = func(gid groupID, accid accountID) {
|
||||
sub.wsh.LeaveRoom(sub.region, gid.Hex(), accid)
|
||||
}
|
||||
|
||||
sub.apiFuncs.registApiFunction("JoinParty", gp.JoinParty)
|
||||
sub.apiFuncs.registApiFunction("InviteToParty", gp.InviteToParty)
|
||||
sub.apiFuncs.registApiFunction("AcceptPartyInvitation", gp.AcceptPartyInvitation)
|
||||
sub.apiFuncs.registApiFunction("DenyPartyInvitation", gp.DenyPartyInvitation)
|
||||
sub.apiFuncs.registApiFunction("QueryPartyMemberState", gp.QueryPartyMemberState)
|
||||
sub.apiFuncs.registApiFunction("LeaveParty", gp.LeaveParty)
|
||||
sub.apiFuncs.registApiFunction("UpdatePartyMemberDocument", gp.UpdatePartyMemberDocument)
|
||||
sub.apiFuncs.registApiFunction("UpdatePartyDocument", gp.UpdatePartyDocument)
|
||||
sub.apiFuncs.registApiFunction("QueryPartyMembers", gp.QueryPartyMembers)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gp *groupParty) RegisterApiFunctions() {
|
||||
|
||||
}
|
||||
|
||||
// JoinParty : 그룹에 참가
|
||||
// - type : 그룹 타입
|
||||
// - 그룹 타입에 맞는 키(주로 _id)
|
||||
// - member_id : 참가 멤버의 아이디
|
||||
// - body : 멤버의 속성 bson document
|
||||
func (gp *groupParty) JoinParty(w http.ResponseWriter, r *http.Request) {
|
||||
doc := bson.M{}
|
||||
if err := readBsonDoc(r.Body, &doc); err != nil {
|
||||
logger.Error("JoinParty failed. readBsonDoc returns err :", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid")
|
||||
if !ok {
|
||||
logger.Println("JoinParty failed. gid is missing :", r.Form)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mid, midok := gocommon.ReadObjectIDFormValue(r.Form, "mid")
|
||||
if !midok {
|
||||
logger.Println("JoinParty failed. mid should be exist")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
gd, err := gp.find(gid)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if gd == nil {
|
||||
// 그룹이 없다. 실패
|
||||
w.Write([]byte("{}"))
|
||||
return
|
||||
}
|
||||
|
||||
// 내 정보 업데이트할 때에도 사용됨
|
||||
if memdoc, err := gd.addMember(mid, doc); err == nil {
|
||||
// 기존 유저에게 새 유저 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: map[string]any{
|
||||
gd.tid(mid): memdoc,
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
|
||||
gp.sendEnterRoomMessage(gid, mid)
|
||||
|
||||
// 새 멤버에 그룹 전체를 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: gd.loadFull(),
|
||||
Tag: []string{"GroupDocFull"},
|
||||
})
|
||||
writeBsonDoc(w, map[string]string{
|
||||
"gid": gid.Hex(),
|
||||
"tid": gd.tid(mid),
|
||||
})
|
||||
} else if err != nil {
|
||||
logger.Error("JoinParty failed :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// InviteToParty : 초대
|
||||
// - type : 초대 타입 (required)
|
||||
// - from : 초대하는 자 (required)
|
||||
// - to : 초대받는 자 (required)
|
||||
// - timeout : 초대 유지시간(optional. 없으면 config 기본 값)
|
||||
// - (body) : 검색시 노출되는 document
|
||||
func (gp *groupParty) InviteToParty(w http.ResponseWriter, r *http.Request) {
|
||||
gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid")
|
||||
if !ok {
|
||||
logger.Println("InviteToParty failed. gid is missing :", r)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mid, ok := gocommon.ReadObjectIDFormValue(r.Form, "mid")
|
||||
if !ok {
|
||||
logger.Println("InviteToParty failed. mid is missing :", r)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var reqdoc struct {
|
||||
Inviter bson.M `bson:"inviter"`
|
||||
Invitee bson.M `bson:"invitee"`
|
||||
}
|
||||
if err := readBsonDoc(r.Body, &reqdoc); err != nil {
|
||||
logger.Println("InviteToParty failed. readBsonDoc returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
targetid, ok := reqdoc.Invitee["_mid"].(accountID)
|
||||
if !ok {
|
||||
logger.Println("InviteToParty failed. invitee mid is missing :", r)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// targetid에 초대한 mid가 들어있다.
|
||||
success, err := gp.rh.SetNX(context.Background(), "inv."+targetid.Hex(), mid.Hex(), time.Duration(gp.InviteExpire)*time.Second).Result()
|
||||
if err != nil {
|
||||
logger.Println("InviteToParty failed. gp.rh.SetNX() return err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !success {
|
||||
// 이미 초대 중이다.
|
||||
// inviter한테 알려줘야 한다.
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: reqdoc.Invitee,
|
||||
Tag: []string{"InvitationFail"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
gd, err := gp.find(gid)
|
||||
if err != nil {
|
||||
logger.Println("InviteToParty failed. gp.find() return err :", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if gd == nil {
|
||||
gd, err = gp.createGroup(gid, mid, reqdoc.Inviter)
|
||||
if err != nil {
|
||||
logger.Println("InviteToParty failed. gp.createGroup() return err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// 내가 wshandler room에 입장
|
||||
gp.sendEnterRoomMessage(gid, mid)
|
||||
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: gd,
|
||||
Tag: []string{"GroupDocFull"},
|
||||
})
|
||||
}
|
||||
|
||||
newdoc, err := gd.addInvite(reqdoc.Invitee, time.Duration(gp.InviteExpire+1)*time.Second, gp.MaxMember)
|
||||
if err != nil {
|
||||
logger.Println("InviteToParty failed. gp.addInvite() return err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// invitee에게 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + targetid.Hex(),
|
||||
Body: Invitation{
|
||||
GroupID: gid,
|
||||
TicketID: gd.tid(targetid),
|
||||
Inviter: reqdoc.Inviter,
|
||||
ExpireAtUTC: newdoc.InviteExpire,
|
||||
},
|
||||
Tag: []string{"Invitation"},
|
||||
})
|
||||
|
||||
w.Write([]byte(gd.strid()))
|
||||
}
|
||||
|
||||
func (gp *groupParty) AcceptPartyInvitation(w http.ResponseWriter, r *http.Request) {
|
||||
gid, _ := gocommon.ReadObjectIDFormValue(r.Form, "gid")
|
||||
mid, _ := gocommon.ReadObjectIDFormValue(r.Form, "mid")
|
||||
|
||||
var member bson.M
|
||||
if err := readBsonDoc(r.Body, &member); err != nil {
|
||||
logger.Error("AcceptPartyInvitation failed. readBsonDoc returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cnt, err := gp.rh.Del(context.Background(), mid.Hex()).Result()
|
||||
if err != nil {
|
||||
logger.Error("AcceptPartyInvitation failed. gp.rh.Del returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
// 만료됨
|
||||
w.Write([]byte("expired"))
|
||||
return
|
||||
}
|
||||
|
||||
gd := &groupDoc{
|
||||
id: gid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
|
||||
memberDoc, err := gd.addMember(mid, member)
|
||||
if err == nil {
|
||||
// 기존 멤버에게 새 멤버를 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: map[string]any{
|
||||
gd.tid(mid): memberDoc,
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
|
||||
gp.sendEnterRoomMessage(gid, mid)
|
||||
|
||||
// 새 멤버에 그룹 전체를 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: gd.loadFull(),
|
||||
Tag: []string{"GroupDocFull"},
|
||||
})
|
||||
} else {
|
||||
logger.Println("AcceptPartyInvitation failed. group.AcceptPartyInvitation returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (gp *groupParty) DenyPartyInvitation(w http.ResponseWriter, r *http.Request) {
|
||||
gid, _ := gocommon.ReadObjectIDFormValue(r.Form, "gid")
|
||||
mid, _ := gocommon.ReadObjectIDFormValue(r.Form, "mid")
|
||||
|
||||
gp.rh.Del(context.Background(), mid.Hex()).Result()
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
gd.removeMember(mid)
|
||||
}
|
||||
|
||||
func (gp *groupParty) QueryPartyMemberState(w http.ResponseWriter, r *http.Request) {
|
||||
mid, ok := gocommon.ReadStringFormValue(r.Form, "mid")
|
||||
if !ok {
|
||||
logger.Println("IsOnline failed. mid is missing :", r.Form)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
state, _ := gp.rh.HGet(gp.rh.Context(), mid, "party_state").Result()
|
||||
w.Write([]byte(state))
|
||||
}
|
||||
|
||||
// LeaveParty : 그룹에서 나감 or 내보냄
|
||||
// - type : 그룹 타입
|
||||
// - 그룹 타입에 맞는 키(주로 _id)
|
||||
// - member_id : 나갈 멤버의 아이디
|
||||
func (gp *groupParty) LeaveParty(w http.ResponseWriter, r *http.Request) {
|
||||
gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid")
|
||||
if !ok {
|
||||
logger.Println("LeaveParty failed. gid is missing :", r.Form)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mid, midok := gocommon.ReadObjectIDFormValue(r.Form, "mid")
|
||||
if !midok {
|
||||
logger.Println("LeaveParty failed. mid is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
if err := gd.removeMember(mid); err != nil {
|
||||
logger.Println("LeaveParty failed. gd.removeMember returns err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 나한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "@" + mid.Hex(),
|
||||
Body: bson.M{"gid": gid},
|
||||
Tag: []string{"GroupDocFull", gid.Hex()},
|
||||
})
|
||||
gp.sendLeaveRoomMessage(gid, mid)
|
||||
}
|
||||
|
||||
func (gp *groupParty) updateMemberDocument(gid groupID, mid accountID, doc bson.M) error {
|
||||
gd := &groupDoc{
|
||||
id: gid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
prefixPath := fmt.Sprintf("$._members.%s.", gd.tid(mid))
|
||||
err := gp.rh.JSONMSetRel(gd.strid(), prefixPath, doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newstate, ok := doc["_state"]; ok {
|
||||
gp.rh.HSet(gp.rh.Context(), mid.Hex(), "party_state", newstate).Result()
|
||||
}
|
||||
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: map[string]any{
|
||||
gd.tid(mid): doc,
|
||||
},
|
||||
Tag: []string{"MemberDocFragment"},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gp *groupParty) UpdatePartyMemberDocument(w http.ResponseWriter, r *http.Request) {
|
||||
mid, ok := gocommon.ReadObjectIDFormValue(r.Form, "mid")
|
||||
if !ok {
|
||||
logger.Println("UpdatePartyMemberDocument failed. member_id is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid")
|
||||
if !ok {
|
||||
logger.Println("UpdatePartyMemberDocument failed. _id is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var updatedoc bson.M
|
||||
if err := readBsonDoc(r.Body, &updatedoc); err != nil {
|
||||
logger.Error("UpdatePartyMemberDocument failed. body decoding error :", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := gp.updateMemberDocument(gid, mid, updatedoc); err != nil {
|
||||
logger.Println("UpdatePartyMemberDocument failed :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (gp *groupParty) updatePartyDocument(gid groupID, frag bson.M) error {
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
if err := gp.rh.JSONMSetRel(gd.strid(), "$.", frag); err != nil {
|
||||
return err
|
||||
}
|
||||
// 업데이트 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + gid.Hex(),
|
||||
Body: frag,
|
||||
Tag: []string{"GroupDocFragment"},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gp *groupParty) UpdatePartyDocument(w http.ResponseWriter, r *http.Request) {
|
||||
gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid")
|
||||
if !ok {
|
||||
logger.Println("UpdatePartyDocument failed. gid is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var frag bson.M
|
||||
if err := readBsonDoc(r.Body, &frag); err != nil {
|
||||
logger.Error("UpdatePartyDocument failed. readBsonDoc err :", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := gp.updatePartyDocument(gid, frag); err != nil {
|
||||
logger.Error("UpdatePartyDocument failed. group.UpdatePartyDocument returns err :", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (gp *groupParty) QueryPartyMembers(w http.ResponseWriter, r *http.Request) {
|
||||
gid, ok := gocommon.ReadObjectIDFormValue(r.Form, "gid")
|
||||
if !ok {
|
||||
logger.Println("QueryPartyMembers failed. gid is missing")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
gd := groupDoc{
|
||||
id: gid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
|
||||
members, err := gd.getMembers()
|
||||
if err != nil {
|
||||
logger.Error("QueryPartyMembers failed. group.QueryPartyMembers returns err :", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := writeBsonDoc(w, members); err != nil {
|
||||
logger.Error("QueryPartyMembers failed. writeBsonDoc return err :", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (gp *groupParty) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*groupDoc, error) {
|
||||
tid := makeTid(newid, charge)
|
||||
|
||||
gd := &groupDoc{
|
||||
Members: map[string]any{
|
||||
tid: &memberDoc{
|
||||
Body: chargeDoc,
|
||||
Invite: false,
|
||||
InviteExpire: 0,
|
||||
},
|
||||
},
|
||||
InCharge: tid,
|
||||
|
||||
rh: gp.rh,
|
||||
id: newid,
|
||||
}
|
||||
|
||||
_, err := gp.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gd, nil
|
||||
}
|
||||
|
||||
func (gp *groupParty) find(id groupID) (*groupDoc, error) {
|
||||
if id.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, err := gp.rh.JSONObjLen(id.Hex(), "$")
|
||||
if err == redis.Nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &groupDoc{
|
||||
rh: gp.rh,
|
||||
id: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (gp *groupParty) memberDisconnected(room string, mid primitive.ObjectID) {
|
||||
gid, err := primitive.ObjectIDFromHex(room)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
gd := &groupDoc{
|
||||
id: gid,
|
||||
rh: gp.rh,
|
||||
}
|
||||
|
||||
tid := gd.tid(mid)
|
||||
deleted, _ := gp.rh.JSONDel(room, "$._members."+tid)
|
||||
if deleted > 0 {
|
||||
// 퇴장을 알림
|
||||
gp.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
||||
Target: "#" + room,
|
||||
Body: bson.M{
|
||||
tid: bson.M{},
|
||||
},
|
||||
Tag: []string{"MemberDocFull"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (gp *groupParty) ClientMessageReceved(sender *wshandler.Sender, mt wshandler.WebSocketMessageType, message any) {
|
||||
if mt == wshandler.Connected {
|
||||
gp.rh.HSet(gp.rh.Context(), sender.Accid.Hex(), "party_state", "connected").Result()
|
||||
} else if mt == wshandler.Disconnected {
|
||||
rooms := message.([]string)
|
||||
for _, roomname := range rooms {
|
||||
gp.memberDisconnected(roomname, sender.Accid)
|
||||
}
|
||||
gp.rh.HDel(gp.rh.Context(), sender.Accid.Hex(), "party_state").Result()
|
||||
} else if mt == wshandler.BinaryMessage {
|
||||
commandline := message.([]any)
|
||||
cmd := commandline[0].(string)
|
||||
args := commandline[1:]
|
||||
switch cmd {
|
||||
case "UpdatePartyMemberDocument":
|
||||
gidobj, _ := primitive.ObjectIDFromHex(args[0].(string))
|
||||
doc := args[1].(map[string]any)
|
||||
gp.updateMemberDocument(gidobj, sender.Accid, doc)
|
||||
|
||||
case "UpdatePartyDocument":
|
||||
gidobj, _ := primitive.ObjectIDFromHex(args[0].(string))
|
||||
doc := args[1].(map[string]any)
|
||||
gp.updatePartyDocument(gidobj, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
113
core/tavern.go
113
core/tavern.go
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -18,19 +19,12 @@ import (
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/bsonrw"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxMemory = 32 << 10 // 32 KB
|
||||
)
|
||||
|
||||
func writeBsonArr(w io.Writer, src []bson.M) error {
|
||||
return writeBsonDoc(w, bson.M{
|
||||
"r": src,
|
||||
})
|
||||
}
|
||||
|
||||
func writeBsonDoc[T any](w io.Writer, src T) error {
|
||||
rw, err := bsonrw.NewBSONValueWriter(w)
|
||||
if err != nil {
|
||||
@ -71,7 +65,7 @@ func readBsonDoc(r io.Reader, src any) error {
|
||||
type TavernConfig struct {
|
||||
gocommon.RegionStorageConfig `json:",inline"`
|
||||
|
||||
GroupTypes map[string]*groupConfig `json:"tavern_group_types"`
|
||||
Group2 map[string]configDocument `json:"tavern_group_types"`
|
||||
MaingateApiToken string `json:"maingate_api_token"`
|
||||
RedisURL string `json:"tavern_redis_url"`
|
||||
macAddr string
|
||||
@ -90,7 +84,7 @@ type subTavern struct {
|
||||
wsh *wshandler.WebsocketHandler
|
||||
region string
|
||||
groups map[string]group
|
||||
methods map[string]reflect.Method
|
||||
apiFuncs *apiFuncsContainer
|
||||
}
|
||||
|
||||
func getMacAddr() (string, error) {
|
||||
@ -147,15 +141,6 @@ func (tv *Tavern) prepare(ctx context.Context) error {
|
||||
for region, addr := range config.RegionStorage {
|
||||
var dbconn gocommon.MongoClient
|
||||
var err error
|
||||
var groupinstance group
|
||||
|
||||
var tmp *subTavern
|
||||
methods := make(map[string]reflect.Method)
|
||||
tp := reflect.TypeOf(tmp)
|
||||
for i := 0; i < tp.NumMethod(); i++ {
|
||||
method := tp.Method(i)
|
||||
methods[method.Name] = method
|
||||
}
|
||||
|
||||
redisClient, err := gocommon.NewRedisClient(addr.Redis["tavern"])
|
||||
if err != nil {
|
||||
@ -167,30 +152,32 @@ func (tv *Tavern) prepare(ctx context.Context) error {
|
||||
mongoClient: dbconn,
|
||||
redisClient: redisClient,
|
||||
region: region,
|
||||
methods: methods,
|
||||
apiFuncs: &apiFuncsContainer{
|
||||
normfuncs: make(map[string]apiFuncType),
|
||||
funcs: make(map[string][]apiFuncType),
|
||||
},
|
||||
}
|
||||
|
||||
groups := make(map[string]group)
|
||||
for typename, cfg := range config.GroupTypes {
|
||||
cfg.Name = typename
|
||||
if cfg.Transient {
|
||||
groupinstance, err = cfg.prepareInMemory(ctx, typename, sub)
|
||||
//} else {
|
||||
// TODO : db
|
||||
// if !dbconn.Connected() {
|
||||
// dbconn, err = gocommon.NewMongoClient(ctx, url.Mongo, region)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// groupinstance, err = cfg.preparePersistent(ctx, region, dbconn, tv.wsh)
|
||||
for typename, cfg := range config.Group2 {
|
||||
gtype, ok := groupTypeContainer()[typename]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s group type is not valid", typename)
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
if !gtype.Implements(reflect.TypeOf((*group)(nil)).Elem()) {
|
||||
return fmt.Errorf("%s is not implement proper interface", typename)
|
||||
}
|
||||
ptrvalue := reflect.New(gtype.Elem())
|
||||
instance := ptrvalue.Interface().(group)
|
||||
if err := instance.Initialize(sub, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
groups[typename] = groupinstance
|
||||
groups[typename] = instance
|
||||
}
|
||||
|
||||
sub.groups = groups
|
||||
sub.apiFuncs.normalize()
|
||||
|
||||
tv.subTaverns = append(tv.subTaverns, sub)
|
||||
}
|
||||
@ -216,22 +203,27 @@ func (tv *Tavern) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux,
|
||||
func (sub *subTavern) OnClientMessageReceived(sender *wshandler.Sender, messageType wshandler.WebSocketMessageType, body io.Reader) {
|
||||
if messageType == wshandler.Connected {
|
||||
logger.Println("OnClientMessageReceived : connected ", sender.Accid.Hex())
|
||||
sub.redisClient.HSet(sub.redisClient.Context(), sender.Accid.Hex(), "_ts", time.Now().UTC().Unix()).Result()
|
||||
|
||||
for _, gt := range sub.groups {
|
||||
gt.ClientMessageReceved(sender, messageType, nil)
|
||||
}
|
||||
} else if messageType == wshandler.Disconnected {
|
||||
var rooms []string
|
||||
dec := json.NewDecoder(body)
|
||||
if err := dec.Decode(&rooms); err == nil {
|
||||
for _, roomname := range rooms {
|
||||
for _, gt := range sub.groups {
|
||||
gt.MemberDisconnected(roomname, sender.Accid)
|
||||
}
|
||||
gt.ClientMessageReceved(sender, messageType, rooms)
|
||||
}
|
||||
}
|
||||
sub.redisClient.Del(sub.redisClient.Context(), sender.Accid.Hex()).Result()
|
||||
logger.Println("OnClientMessageReceived : disconnected ", sender.Accid.Hex())
|
||||
} else if messageType == wshandler.BinaryMessage {
|
||||
var msg map[string][]any
|
||||
var commandline []any
|
||||
dec := json.NewDecoder(body)
|
||||
if err := dec.Decode(&msg); err == nil {
|
||||
for cmd, args := range msg {
|
||||
if err := dec.Decode(&commandline); err == nil {
|
||||
cmd := commandline[0].(string)
|
||||
args := commandline[1:]
|
||||
switch cmd {
|
||||
case "EnterChannel":
|
||||
sub.wsh.EnterRoom(sub.region, args[0].(string), sender.Accid)
|
||||
@ -239,21 +231,9 @@ func (sub *subTavern) OnClientMessageReceived(sender *wshandler.Sender, messageT
|
||||
case "LeaveChannel":
|
||||
sub.wsh.LeaveRoom(sub.region, args[0].(string), sender.Accid)
|
||||
|
||||
case "UpdateGroupMemberDocument":
|
||||
typename := args[0].(string)
|
||||
gidobj, _ := primitive.ObjectIDFromHex(args[1].(string))
|
||||
doc := args[2].(map[string]any)
|
||||
if group := sub.groups[typename]; group != nil {
|
||||
group.UpdateMemberDocument(gidobj, sender.Accid, doc)
|
||||
}
|
||||
|
||||
case "UpdateGroupDocument":
|
||||
typename := args[0].(string)
|
||||
gidobj, _ := primitive.ObjectIDFromHex(args[1].(string))
|
||||
doc := args[2].(map[string]any)
|
||||
if group := sub.groups[typename]; group != nil {
|
||||
group.UpdateGroupDocument(gidobj, doc)
|
||||
}
|
||||
default:
|
||||
for _, gt := range sub.groups {
|
||||
gt.ClientMessageReceved(sender, messageType, commandline)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -292,6 +272,9 @@ func (sub *subTavern) api(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if r.PostForm == nil {
|
||||
r.ParseMultipartForm(defaultMaxMemory)
|
||||
}
|
||||
|
||||
operation := r.URL.Query().Get("operation")
|
||||
if len(operation) == 0 {
|
||||
@ -299,23 +282,5 @@ func (sub *subTavern) api(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
method, ok := sub.methods[operation]
|
||||
if !ok {
|
||||
// 없는 operation
|
||||
logger.Println("fail to call api. operation is not valid :", operation)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.PostForm == nil {
|
||||
r.ParseMultipartForm(defaultMaxMemory)
|
||||
}
|
||||
|
||||
args := []reflect.Value{
|
||||
reflect.ValueOf(sub),
|
||||
reflect.ValueOf(w),
|
||||
reflect.ValueOf(r),
|
||||
}
|
||||
|
||||
method.Func.Call(args)
|
||||
sub.apiFuncs.call(operation, w, r)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user