package core import ( "context" "encoding/gob" "net/http" "time" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo/options" "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/logger" "repositories.action2quare.com/ayo/gocommon/wshandler" ) type invitation struct { mongoClient gocommon.MongoClient redison *gocommon.RedisonHandler wsh *wshandler.WebsocketHandler f *friends } type invitationDoc struct { Id primitive.ObjectID `bson:"_id,omitempty" json:"_id"` From primitive.ObjectID `bson:"from,omitempty" json:"-"` To primitive.ObjectID `bson:"to,omitempty" json:"-"` FromAlias string `bson:"falias,omitempty" json:"from"` ToAlias string `bson:"talias,omitempty" json:"to"` Timestamp int64 `bson:"ts" json:"ts"` Denied bool `bson:"denied,omitempty" json:"denied,omitempty"` Canceled bool `bson:"canceled,omitempty" json:"canceled,omitempty"` Blocked bool `bson:"blocked,omitempty" json:"-"` // From은 To에 의해 차단된 상태를 표시 } type sneakpeekDoc struct { From primitive.ObjectID `bson:"from,omitempty" json:"-"` To primitive.ObjectID `bson:"to,omitempty" json:"-"` } func init() { gob.Register([]invitationDoc{}) } func makeInvitation(ctx context.Context, s *Social, f *friends) (*invitation, error) { if err := s.mongoClient.MakeUniqueIndices(invitation_collection_name, map[string]bson.D{ "fromts": {{Key: "from", Value: 1}, {Key: "ts", Value: -1}}, }); err != nil { return nil, err } // 내가 받은거 if err := s.mongoClient.MakeIndices(invitation_collection_name, map[string]bson.D{ "tots": {{Key: "to", Value: 1}, {Key: "ts", Value: -1}}, }); err != nil { return nil, err } return &invitation{ mongoClient: s.mongoClient, redison: s.redison, wsh: s.wsh, f: f, }, nil } func (iv *invitation) QueryInvitations(ctx wshandler.ApiCallContext) { // 내가 받은 초대 목록 queryfrom := int64(ctx.Arguments[0].(float64)) var receives []*invitationDoc if err := iv.mongoClient.FindAllAs(invitation_collection_name, bson.M{ "to": ctx.CallBy.Accid, "ts": bson.M{"$gt": queryfrom}, "blocked": bson.M{"$exists": false}, "denied": bson.M{"$exists": false}, }, &receives, options.Find().SetHint("tots")); err != nil { logger.Println("QueryInvitations failed. FindAllAs err :", err) } var sents []*invitationDoc if err := iv.mongoClient.FindAllAs(invitation_collection_name, bson.M{ "from": ctx.CallBy.Accid, "ts": bson.M{"$gt": queryfrom}, "canceled": bson.M{"$exists": false}, }, &sents, options.Find().SetHint("fromts")); err != nil { logger.Println("QueryInvitations failed. FindAllAs err :", err) } invitations := append(receives, sents...) if len(invitations) > 0 { iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ Target: ctx.CallBy.Accid.Hex(), Body: invitations, Tag: invitations_tag, }) } } func (iv *invitation) CancelInvitation(ctx wshandler.ApiCallContext) { // ctx.CallBy.Accid invId, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) now := time.Now().UTC().Unix() var ivdoc invitationDoc if err := iv.mongoClient.FindOneAndUpdateAs(invitation_collection_name, bson.M{ "_id": invId, "from": bson.M{"$eq": ctx.CallBy.Accid}, }, bson.M{ "$set": bson.M{ "canceled": true, "ts": now, }, }, &ivdoc, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(false)); err != nil { logger.Println("CancelInvitation failed :", err) return } if ivdoc.Id.IsZero() { return } if ivdoc.Blocked { // 차단된 초대다. 초대한 사람은 차단된 상태를 모르기 때문에 이 초대는 삭제하고, 초대받은 사람한테는 보내지 않는다. iv.mongoClient.Delete(invitation_collection_name, bson.M{"_id": invId}) } else { iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ Target: ivdoc.To.Hex(), Body: []invitationDoc{ivdoc}, Tag: invitations_tag, }) } } func (iv *invitation) AcceptInvitation(ctx wshandler.ApiCallContext) { invId, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) var ivdoc invitationDoc if err := iv.mongoClient.FindOneAs(invitation_collection_name, bson.M{ "_id": invId, }, &ivdoc); err != nil { logger.Println("AcceptInvitation failed:", err) return } if ivdoc.Id != invId { // 초대가 없다 return } if ivdoc.To != ctx.CallBy.Accid { // 내가 받은 초대가 아니네? return } now := time.Now().UTC().Unix() f1 := friendDoc{ From: ivdoc.To, // 수락한 나 To: ivdoc.From, // 상대방 ToAlias: ivdoc.FromAlias, Timestamp: now, } f2 := friendDoc{ From: ivdoc.From, // 상대방 To: ivdoc.To, // 나 ToAlias: ivdoc.ToAlias, Timestamp: now, } // 나한테 상대방을 친구로 만든다 // SendUpstreamMessage를 먼저 해야 함 iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ Target: f1.From.Hex(), Body: []friendDoc{f1}, Tag: friends_tag, }) iv.f.addFriend(&f1) // 상대방한테 나를 친구로 만듬 iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ Target: f2.From.Hex(), Body: []friendDoc{f2}, Tag: friends_tag, }) iv.f.addFriend(&f2) iv.mongoClient.Delete(invitation_collection_name, bson.M{"_id": invId}) } func (iv *invitation) DenyInvitation(ctx wshandler.ApiCallContext) { invId, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) now := time.Now().UTC().Unix() var ivdoc invitationDoc if err := iv.mongoClient.FindOneAndUpdateAs(invitation_collection_name, bson.M{ "_id": invId, "to": bson.M{"$eq": ctx.CallBy.Accid}, }, bson.M{ "$set": bson.M{ "denied": true, "ts": now, }, }, &ivdoc, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(false)); err != nil { logger.Println("DenyInvitation failed. addFriend(f2) err :", err) return } if ivdoc.Id.IsZero() { // 없다 return } iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ Target: ivdoc.From.Hex(), Body: []invitationDoc{ivdoc}, Tag: invitations_tag, }) } func (iv *invitation) Trim(ctx wshandler.ApiCallContext) { ids := stringsToObjs(ctx.Arguments[2].([]any)) if len(ids) > 0 { if len(ids) == 1 { iv.mongoClient.Delete(block_collection_name, bson.M{"_id": ids[0]}) } else { iv.mongoClient.DeleteMany(block_collection_name, bson.D{ {Key: "_id", Value: bson.M{"$in": ids}}, }) } } } func (iv *invitation) SneakPeekTarget(w http.ResponseWriter, r *http.Request) { var ivdoc sneakpeekDoc if err := gocommon.MakeDecoder(r).Decode(&ivdoc); err != nil { logger.Println("InviteAsFriend failed:", err) w.WriteHeader(http.StatusBadRequest) return } // ivdoc.To가 invdoc.From을 차단했으면 offline으로 표시 exists, err := iv.mongoClient.Exists(block_collection_name, bson.M{"_id": combineObjectID(ivdoc.To, ivdoc.From)}) if err != nil { logger.Println("InviteAsFriend failed:", err) w.WriteHeader(http.StatusInternalServerError) return } enc := gocommon.MakeEncoder(w, r) if exists { enc.Encode("offline") } else { exists, _ := iv.redison.Exists(iv.redison.Context(), ivdoc.To.Hex()).Result() if exists == 0 { enc.Encode("offline") } else { enc.Encode("online") } } } func (iv *invitation) InviteAsFriend(w http.ResponseWriter, r *http.Request) { // 내 현재 친구 숫자 + 내가 보낸 초대 숫자가 FriendsMax를 넘을 수 없다. // TODO : 이미 친구면 초대 불가 var ivdoc invitationDoc if err := gocommon.MakeDecoder(r).Decode(&ivdoc); err != nil { logger.Println("InviteAsFriend failed:", err) w.WriteHeader(http.StatusBadRequest) return } if exists, err := iv.mongoClient.Exists(friends_collection_name, bson.M{"_id": combineObjectID(ivdoc.From, ivdoc.To)}); err != nil { logger.Println("InviteAsFriend failed:", err) w.WriteHeader(http.StatusBadRequest) return } else if exists { // 이미 친구 w.WriteHeader(http.StatusBadRequest) return } // ivdoc.To가 invdoc.From을 차단했으면 표시 exists, err := iv.mongoClient.Exists(block_collection_name, bson.M{"_id": combineObjectID(ivdoc.To, ivdoc.From)}) if err != nil { logger.Println("InviteAsFriend failed:", err) w.WriteHeader(http.StatusInternalServerError) return } ivdoc.Blocked = exists ivdoc.Timestamp = time.Now().UTC().Unix() _, newid, err := iv.mongoClient.Update(invitation_collection_name, bson.M{ "_id": combineObjectID(ivdoc.From, ivdoc.To), }, bson.M{"$setOnInsert": ivdoc}, options.Update().SetUpsert(true)) if err != nil { logger.Println("InviteAsFriend failed:", err) w.WriteHeader(http.StatusInternalServerError) return } if newid == nil { // 이미 보낸 초대 요청 return } ivdoc.Id = newid.(primitive.ObjectID) if !ivdoc.Blocked { iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ Target: ivdoc.To.Hex(), Body: []invitationDoc{ivdoc}, Tag: invitations_tag, }) } iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ Target: ivdoc.From.Hex(), Body: []invitationDoc{ivdoc}, Tag: invitations_tag, }) } func (iv *invitation) Block(w http.ResponseWriter, r *http.Request) { var block blockDoc if err := gocommon.MakeDecoder(r).Decode(&block); err != nil { logger.Println("Block failed:", err) w.WriteHeader(http.StatusBadRequest) return } // 차단한 상대가 나한테 보낸 초대가 있으면 차단 표시 // block.From이 block.To를 차단 -> block.To가 block.From을 초대한게 있나? id := combineObjectID(block.To, block.From) now := time.Now().UTC().Unix() updated, _, err := iv.mongoClient.Update(invitation_collection_name, bson.M{ "_id": id, }, bson.M{ "$set": bson.M{ "blocked": true, "ts": now, }, }, options.Update().SetUpsert(false)) if err != nil { logger.Println("Block failed:", err) w.WriteHeader(http.StatusInternalServerError) return } if updated { // 초대가 있었다. // 사실은 삭제가 아니지만 초대 삭제 알림. 나중에 쿼리해도 안나옴 iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ Target: block.From.Hex(), Body: []invitationDoc{{Id: id, Canceled: true, Timestamp: now}}, Tag: invitations_tag, }) } } func (iv *invitation) Unblock(ctx wshandler.ApiCallContext) { // From이 To를 unblock = to가 from을 block했었다. = from이 to한테 보낸 초대가 있을 수 있다. // invitation key는 to+from이고 block key는 from+to id, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) var revertid primitive.ObjectID copy(revertid[:6], id[6:]) copy(revertid[6:], id[:6]) now := time.Now().UTC().Unix() var ivdoc invitationDoc err := iv.mongoClient.FindOneAndUpdateAs(invitation_collection_name, bson.M{ "_id": revertid, }, bson.M{ "$set": bson.M{ "ts": now, }, "$unset": bson.M{ "canceled": "", "blocked": "", }, }, &ivdoc, options.FindOneAndUpdate().SetUpsert(false).SetReturnDocument(options.After)) if err != nil { logger.Println("Block failed:", err) return } if !ivdoc.Id.IsZero() { // 받은 초대가 있었다. // 나한테 알림 ivdoc.Canceled = false iv.f.conns.writeMessage(ctx.CallBy.Accid, &wshandler.DownstreamMessage{ Body: []invitationDoc{ivdoc}, Tag: invitations_tag, }) } }