Skip to content

Commit c0fa474

Browse files
committed
feat: Add chats support & add CreateChat method for chat service
1 parent 430cc8c commit c0fa474

15 files changed

+3355
-440
lines changed

chats/chats.graphqls

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
type Chat {
2+
id: String!
3+
user: User!
4+
messages(count: Int): [Message]
5+
messagesCount: Int!
6+
lastViewedMessage: LastViewedMessage
7+
}
8+
9+
enum MessageType {
10+
Text
11+
Reply
12+
GIF
13+
Image
14+
Images
15+
Sticker
16+
File
17+
Files
18+
Withdrawn
19+
}
20+
21+
type Message {
22+
id: String!
23+
user: User!
24+
messageType: MessageType!
25+
content: String!
26+
sendTime: Int!
27+
reactions: [Reaction]
28+
isForwarded: Boolean!
29+
}
30+
31+
type Reaction {
32+
emoji: String!
33+
user: User!
34+
}
35+
36+
type LastViewedMessage {
37+
message: Message!
38+
user: User!
39+
time: Int!
40+
}
41+
42+
input ChatFilter {
43+
id: String
44+
userId: String
45+
}
46+
47+
extend type Query {
48+
chatById(id: String!): Chat!
49+
chats(filter: ChatFilter): [Chat]
50+
}
51+
52+
extend type Mutation {
53+
createChat(userId: String!): Chat!
54+
deleteChat(id: String!): Boolean
55+
sendMessage(chatId: String!, messageType: MessageType!, content: String!): Message!
56+
deleteMessage(id: String!, chatId: String!): Boolean
57+
undoMessage(id: String!, chatId: String!): Boolean
58+
setMessageReaction(chatId: String!, messageId: String!, emoji: String!): Reaction!
59+
deleteMessageReaction(chatId: String!, messageId: String!): Boolean
60+
}
61+
62+
extend type Subscription {
63+
chatById(id: String!): Chat!
64+
chats(filter: ChatFilter): [Chat]
65+
}

chats/chats.repository.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package chats
2+
3+
import (
4+
"context"
5+
database "messagewith-server/chats/database"
6+
)
7+
8+
type R interface {
9+
FindOne(ctx context.Context, filter interface{}) (*database.Chat, error)
10+
Create(ctx context.Context, document *database.Chat) error
11+
}
12+
13+
type Repository struct{}
14+
15+
func (r *Repository) FindOne(ctx context.Context, filter interface{}) (*database.Chat, error) {
16+
chat := &database.Chat{}
17+
err := collection.FindOne(ctx, filter).Decode(chat)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
return chat, nil
23+
}
24+
25+
func (r *Repository) Create(ctx context.Context, document *database.Chat) error {
26+
err := collection.CreateWithCtx(ctx, document)
27+
28+
return err
29+
}

chats/chats.service.go

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package chats
2+
3+
import (
4+
"context"
5+
"go.mongodb.org/mongo-driver/bson"
6+
"go.mongodb.org/mongo-driver/bson/primitive"
7+
database "messagewith-server/chats/database"
8+
errorConstants "messagewith-server/error-constants"
9+
"messagewith-server/graph/model"
10+
"messagewith-server/users"
11+
usersDatabase "messagewith-server/users/database"
12+
)
13+
14+
var (
15+
repository R
16+
cachedUsers = map[string]*model.User{}
17+
)
18+
19+
type service struct{}
20+
21+
func getService(repo R) *service {
22+
repository = repo
23+
return &service{}
24+
}
25+
26+
func (service *service) CreateChat(ctx context.Context, user *usersDatabase.User, secondUserID string) (*model.Chat, error) {
27+
secondUserObjectID, err := primitive.ObjectIDFromHex(secondUserID)
28+
if err != nil {
29+
return nil, errorConstants.ErrInvalidID
30+
}
31+
32+
_, err = repository.FindOne(ctx, bson.M{"users": bson.M{"$size": 2, "$in": []primitive.ObjectID{user.ID, secondUserObjectID}}})
33+
if err == nil {
34+
return nil, errorConstants.ErrChatAlreadyCreated
35+
}
36+
37+
secondUser, err := users.Service.GetPlainUser(ctx, &secondUserID, nil, nil)
38+
if err != nil {
39+
return nil, errorConstants.ErrInvalidID
40+
}
41+
42+
chat := &database.Chat{
43+
ID: primitive.NewObjectID(),
44+
DeletedBy: []primitive.ObjectID{},
45+
LastViewedMessages: []database.LastViewedMessage{},
46+
Messages: []*database.Message{},
47+
MessagesCount: 0,
48+
Users: []primitive.ObjectID{user.ID, secondUserObjectID},
49+
}
50+
51+
err = repository.Create(ctx, chat)
52+
if err != nil {
53+
panic(err)
54+
}
55+
56+
return FilterChat(ctx, chat, user.ID, secondUser), nil
57+
}
58+
59+
func FilterChat(ctx context.Context, chat *database.Chat, userObjectID primitive.ObjectID, secondUser *usersDatabase.User) *model.Chat {
60+
modelChat := &model.Chat{}
61+
modelChat.MessagesCount = int(chat.MessagesCount)
62+
modelChat.User = users.FilterUser(secondUser)
63+
modelChat.ID = chat.ID.Hex()
64+
modelChat.Messages = FilterAllMessages(ctx, userObjectID, chat.Messages)
65+
modelChat.LastViewedMessage = FilterLastViewedMessage(ctx, chat.LastViewedMessages, chat.Messages)
66+
67+
return modelChat
68+
}
69+
70+
func GetChatUser(ctx context.Context, userID primitive.ObjectID) *model.User {
71+
var filteredUser *model.User
72+
itemIdHex := userID.Hex()
73+
74+
if cachedUsers[itemIdHex] != nil {
75+
filteredUser = cachedUsers[itemIdHex]
76+
} else {
77+
user, _ := users.Service.GetUser(ctx, &itemIdHex, nil, nil)
78+
cachedUsers[itemIdHex] = user
79+
filteredUser = user
80+
}
81+
82+
return filteredUser
83+
}
84+
85+
func GetMessage(allMessages []*database.Message, id primitive.ObjectID) *database.Message {
86+
for _, item := range allMessages {
87+
if item.ID == id {
88+
return item
89+
}
90+
}
91+
92+
return nil
93+
}
94+
95+
func FilterLastViewedMessage(ctx context.Context, lastViewedMessages []database.LastViewedMessage, allMessages []*database.Message) *model.LastViewedMessage {
96+
if len(lastViewedMessages) == 1 {
97+
return &model.LastViewedMessage{
98+
Message: FilterMessage(ctx, nil, GetMessage(allMessages, lastViewedMessages[0].Message)),
99+
User: GetChatUser(ctx, lastViewedMessages[0].User),
100+
Time: int(lastViewedMessages[0].Time.Time().Unix()),
101+
}
102+
}
103+
104+
return nil
105+
}
106+
107+
func FilterMessage(ctx context.Context, userObjectID *primitive.ObjectID, message *database.Message) *model.Message {
108+
for _, deletedBy := range message.DeletedBy {
109+
if deletedBy == *userObjectID {
110+
return nil
111+
}
112+
}
113+
114+
var filteredUser *model.User
115+
messageIdHex := message.ID.Hex()
116+
117+
return &model.Message{
118+
ID: messageIdHex,
119+
User: filteredUser,
120+
MessageType: model.MessageType(message.Type),
121+
Content: message.Content,
122+
IsForwarded: message.IsForwarded,
123+
Reactions: FilterAllReactions(ctx, message.Reactions),
124+
SendTime: int(message.SendTime.Time().Unix()),
125+
}
126+
}
127+
128+
func FilterAllMessages(ctx context.Context, userObjectID primitive.ObjectID, messages []*database.Message) []*model.Message {
129+
allModelMessages := make([]*model.Message, 0)
130+
131+
for _, item := range messages {
132+
filteredMessage := FilterMessage(ctx, &userObjectID, item)
133+
if filteredMessage == nil {
134+
continue
135+
}
136+
137+
allModelMessages = append(allModelMessages, filteredMessage)
138+
}
139+
140+
return allModelMessages
141+
}
142+
143+
func FilterAllReactions(ctx context.Context, reactions []*database.Reaction) []*model.Reaction {
144+
allModelReactions := make([]*model.Reaction, 0)
145+
146+
for _, item := range reactions {
147+
filteredUser := GetChatUser(ctx, item.User)
148+
149+
allModelReactions = append(allModelReactions, &model.Reaction{
150+
User: filteredUser,
151+
Emoji: item.Emoji,
152+
})
153+
}
154+
155+
return allModelReactions
156+
}

chats/database/chats.database.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package chatsDatabase
2+
3+
import (
4+
"github.com/kamva/mgm/v3"
5+
"go.mongodb.org/mongo-driver/bson/primitive"
6+
)
7+
8+
type Chat struct {
9+
mgm.DefaultModel `bson:",inline"`
10+
ID primitive.ObjectID `bson:"_id"`
11+
Users []primitive.ObjectID `bson:"users"`
12+
Messages []*Message `bson:"messages"`
13+
MessagesCount uint32 `bson:"messagesCount"`
14+
LastViewedMessages []LastViewedMessage `bson:"lastViewedMessages"`
15+
DeletedBy []primitive.ObjectID `bson:"deletedBy"`
16+
}
17+
18+
type MessageType struct {
19+
Text string
20+
Reply string
21+
File string
22+
Files string
23+
Image string
24+
Images string
25+
Sticker string
26+
GIF string
27+
Withdrawn string
28+
}
29+
30+
var (
31+
messageType = MessageType{
32+
Text: "plaintext",
33+
Reply: "reply",
34+
GIF: "gif",
35+
Image: "image",
36+
Images: "images",
37+
Sticker: "sticker",
38+
File: "file",
39+
Files: "files",
40+
Withdrawn: "withdrawn",
41+
}
42+
)
43+
44+
type Message struct {
45+
ID primitive.ObjectID `bson:"_id"`
46+
User primitive.ObjectID `bson:"user"`
47+
Type string `bson:"type"`
48+
Content string `bson:"content"`
49+
SendTime primitive.DateTime `bson:"sendTime"`
50+
Reactions []*Reaction `bson:"reactions"`
51+
IsForwarded bool `bson:"isForwarded"`
52+
DeletedBy []primitive.ObjectID `bson:"deletedBy"`
53+
}
54+
55+
type Reaction struct {
56+
Emoji string `bson:"emoji"`
57+
User primitive.ObjectID `bson:"user"`
58+
}
59+
60+
type LastViewedMessage struct {
61+
Message primitive.ObjectID `bson:"message"`
62+
User primitive.ObjectID `bson:"user"`
63+
Time primitive.DateTime `bson:"time"`
64+
}
65+
66+
type DB struct{}
67+
68+
// GetDB Returns new DB instance
69+
func GetDB() *DB {
70+
return &DB{}
71+
}
72+
73+
// UseCollection Returns users *mgm.Collection
74+
func (usersDB *DB) UseCollection() *mgm.Collection {
75+
return mgm.Coll(&Chat{})
76+
}

chats/init.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package chats
2+
3+
import (
4+
"github.com/kamva/mgm/v3"
5+
database "messagewith-server/chats/database"
6+
)
7+
8+
var (
9+
Service *service
10+
collection *mgm.Collection
11+
)
12+
13+
func InitService() {
14+
Service = getService(&Repository{})
15+
collection = database.GetDB().UseCollection()
16+
}

error-constants/errors.go

+1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ var (
2828
ErrInvalidID = errors.New("invalid id")
2929
ErrChangePasswordTokenNotFound = errors.New("invalid token")
3030
ErrChangePasswordSameNewPassword = errors.New("specified new password is the same as old password")
31+
ErrChatAlreadyCreated = errors.New("chat is already created")
3132
)

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ go 1.17
55
require (
66
github.com/99designs/gqlgen v0.15.1
77
github.com/gin-gonic/gin v1.7.7
8+
github.com/gorilla/websocket v1.4.2
89
github.com/joho/godotenv v1.4.0
910
github.com/kamva/mgm/v3 v3.4.1
1011
github.com/mileusna/useragent v1.0.2
12+
github.com/naamancurtis/mongo-go-struct-to-bson v0.1.0
1113
github.com/oschwald/geoip2-golang v1.5.0
1214
github.com/satori/go.uuid v1.2.0
1315
github.com/stretchr/testify v1.7.0
@@ -28,7 +30,6 @@ require (
2830
github.com/go-stack/stack v1.8.0 // indirect
2931
github.com/golang/protobuf v1.4.2 // indirect
3032
github.com/golang/snappy v0.0.1 // indirect
31-
github.com/gorilla/websocket v1.4.2 // indirect
3233
github.com/hashicorp/golang-lru v0.5.0 // indirect
3334
github.com/jinzhu/inflection v1.0.0 // indirect
3435
github.com/json-iterator/go v1.1.9 // indirect
@@ -39,7 +40,6 @@ require (
3940
github.com/mitchellh/mapstructure v1.2.3 // indirect
4041
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
4142
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
42-
github.com/naamancurtis/mongo-go-struct-to-bson v0.1.0 // indirect
4343
github.com/oschwald/maxminddb-golang v1.8.0 // indirect
4444
github.com/pkg/errors v0.9.1 // indirect
4545
github.com/pmezard/go-difflib v1.0.0 // indirect

0 commit comments

Comments
 (0)