Private
Public Access
1
0
Files
Kordophone/server/server.go

358 lines
8.7 KiB
Go

package server
import (
"bytes"
_ "embed"
"io"
"os"
"path"
"sort"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"go.buzzert.net/kordophone-mock/data"
"go.buzzert.net/kordophone-mock/model"
"go.buzzert.net/kordophone-mock/resources"
)
const VERSION string = "KordophoneMock-2.6"
var ATTACHMENT_CONVO_DISP_NAME string = "Attachments"
const (
AUTH_USERNAME = "test"
AUTH_PASSWORD = "test"
)
type Server struct {
version string
conversations []model.Conversation
authTokens []model.AuthToken
attachmentStore model.AttachmentStore
messageStore map[string][]model.Message
updateItems map[int]model.UpdateItem
updateChannels []chan []model.UpdateItem
updateItemSeq int
}
type MessagesQuery struct {
ConversationGUID string
BeforeDate *model.Date
AfterGUID *string
BeforeGUID *string
Limit *int
}
type AuthError struct {
message string
}
func (e *AuthError) Error() string {
return e.message
}
type DatabaseError struct {
message string
}
func (e *DatabaseError) Error() string {
return e.message
}
func NewServer() *Server {
attachmentStorePath := path.Join(os.TempDir(), "kpmock", "attachments")
return &Server{
version: VERSION,
conversations: []model.Conversation{},
authTokens: []model.AuthToken{},
attachmentStore: model.NewAttachmentStore(attachmentStorePath),
messageStore: make(map[string][]model.Message),
updateItems: make(map[int]model.UpdateItem),
updateChannels: []chan []model.UpdateItem{},
updateItemSeq: 0,
}
}
func (s *Server) Version() string {
return s.version
}
func (s *Server) Conversations() []model.Conversation {
return s.conversations
}
func (s *Server) SortedConversations() []model.Conversation {
conversations := s.Conversations()
sort.Slice(conversations, func(i, j int) bool {
return conversations[i].Date.After(conversations[j].Date)
})
return conversations
}
func (s *Server) ConversationForGUID(guid string) (*model.Conversation, error) {
var conversation *model.Conversation = nil
for i := range s.conversations {
c := &s.conversations[i]
if c.Guid == guid {
conversation = c
break
}
}
if conversation != nil {
return conversation, nil
}
return nil, &DatabaseError{message: "Conversation not found"}
}
func (s *Server) AddConversation(c model.Conversation) {
s.conversations = append(s.conversations, c)
}
func (s *Server) PopulateWithTestData() {
numConversations := 100
cs := make([]model.Conversation, numConversations)
for i := 0; i < numConversations; i++ {
cs[i] = data.GenerateRandomConversation()
// Generate messages
convo := &cs[i]
var lastMessage model.Message
for i := 0; i < 100; i++ {
message := data.GenerateRandomMessage(convo.Participants)
s.AppendMessageToConversation(convo, message)
if lastMessage.Date.Before(message.Date) {
lastMessage = message
}
}
// Update last message
convo.LastMessage = lastMessage
convo.LastMessagePreview = lastMessage.Text
}
// Also add an "attachment" conversation
attachmentConversation := model.Conversation{
Participants: []string{"Attachments"},
DisplayName: &ATTACHMENT_CONVO_DISP_NAME,
UnreadCount: 0,
Guid: uuid.New().String(),
Date: model.Date(time.Now()),
}
cs = append(cs, attachmentConversation)
reader := bytes.NewReader(resources.TestAttachmentData)
attachmentGUID, err := s.attachmentStore.StoreAttachment("test.jpg", reader)
if err != nil {
log.Fatal().Msgf("Error storing test attachment: %s", err)
} else {
attachmentMessage := data.GenerateAttachmentMessage(attachmentConversation.Participants, *attachmentGUID)
attachmentMessage.Date = model.Date(time.Now())
s.AppendMessageToConversation(&attachmentConversation, attachmentMessage)
}
s.conversations = cs
}
func (s *Server) Authenticate(username string, password string) (*model.AuthToken, error) {
if username != AUTH_USERNAME || password != AUTH_PASSWORD {
return nil, &AuthError{"Invalid username or password"}
}
token, err := model.NewAuthToken(username)
if err != nil {
return nil, err
}
// Register for future auth
s.registerAuthToken(token)
return token, nil
}
func (s *Server) CheckBearerToken(token string) bool {
return s.authenticateToken(token)
}
func (s *Server) PerformMessageQuery(query *MessagesQuery) []model.Message {
messages := s.messageStore[query.ConversationGUID]
// Sort
sort.Slice(messages, func(i int, j int) bool {
return messages[i].Date.Before(messages[j].Date)
})
// Apply before/after filters
// The following code assumes the messages are sorted by date ascending
if query.BeforeGUID != nil {
beforeGUID := *query.BeforeGUID
for i := range messages {
if messages[i].Guid == beforeGUID {
messages = messages[:i]
break
}
}
} else if query.AfterGUID != nil {
afterGUID := *query.AfterGUID
for i := range messages {
if messages[i].Guid == afterGUID {
messages = messages[i+1:]
break
}
}
} else if query.BeforeDate != nil {
beforeDate := *query.BeforeDate
for i := range messages {
if messages[i].Date.Before(beforeDate) {
messages = messages[:i]
break
}
}
}
// Limit
if query.Limit != nil {
limit := *query.Limit
if len(messages) > limit {
messages = messages[len(messages)-limit:]
}
}
return messages
}
func (s *Server) MessagesForConversation(conversation *model.Conversation) []model.Message {
return s.PerformMessageQuery(&MessagesQuery{
ConversationGUID: conversation.Guid,
})
}
func (s *Server) AppendMessageToConversation(conversation *model.Conversation, message model.Message) {
s.messageStore[conversation.Guid] = append(s.messageStore[conversation.Guid], message)
}
func (s *Server) SendMessage(conversation *model.Conversation, message model.Message) {
s.AppendMessageToConversation(conversation, message)
// Update Conversation
ourConversation, _ := s.ConversationForGUID(conversation.Guid)
ourConversation.LastMessagePreview = message.Text
ourConversation.Date = message.Date
log.Info().EmbedObject(message).Msgf("Sent message to conversation %s", conversation.Guid)
// Enqueue Update
s.EnqueueUpdateItem(model.UpdateItem{
Conversation: ourConversation,
Message: nil, // not what I would do today, but this is what the server does
})
}
func (s *Server) ReceiveMessage(conversation *model.Conversation, message model.Message) {
s.AppendMessageToConversation(conversation, message)
// Update conversation
ourConversation, _ := s.ConversationForGUID(conversation.Guid)
ourConversation.LastMessagePreview = message.Text
ourConversation.Date = message.Date
ourConversation.UnreadCount += 1
// Enqueue Update
s.EnqueueUpdateItem(model.UpdateItem{
Conversation: ourConversation,
Message: &message,
})
log.Info().EmbedObject(message).Msgf("Received message from conversation %s", conversation.Guid)
}
func (s *Server) EnqueueUpdateItem(item model.UpdateItem) {
log.Info().EmbedObject(&item).Msg("Enqueuing update item")
s.updateItemSeq += 1
item.MessageSequenceNumber = s.updateItemSeq
s.updateItems[s.updateItemSeq] = item
// Publish to channel
for i := range s.updateChannels {
s.updateChannels[i] <- []model.UpdateItem{item}
}
}
func (s *Server) FetchUpdates(since int) []model.UpdateItem {
items := []model.UpdateItem{}
for i := since; i <= s.updateItemSeq; i++ {
if val, ok := s.updateItems[i]; ok {
items = append(items, val)
}
}
return items
}
func (s *Server) FetchUpdatesBlocking(since int) []model.UpdateItem {
if since < 0 || since >= s.updateItemSeq {
// Wait for updates
log.Info().Msgf("Waiting for updates since %d", since)
updateChannel := make(chan []model.UpdateItem)
s.updateChannels = append(s.updateChannels, updateChannel)
items := <-updateChannel
// Remove channel
for i := range s.updateChannels {
if s.updateChannels[i] == updateChannel {
s.updateChannels = append(s.updateChannels[:i], s.updateChannels[i+1:]...)
break
}
}
return items
} else {
return s.FetchUpdates(since)
}
}
func (s *Server) MarkConversationAsRead(conversation *model.Conversation) {
conversation.UnreadCount = 0
// enqueue update
s.EnqueueUpdateItem(model.UpdateItem{
Conversation: conversation,
})
}
func (s *Server) FetchAttachment(guid string) (io.Reader, error) {
return s.attachmentStore.FetchAttachment(guid)
}
func (s *Server) UploadAttachment(filename string, reader io.Reader) (*string, error) {
return s.attachmentStore.StoreAttachment(filename, reader)
}
func (s *Server) DeleteAttachment(guid string) error {
return s.attachmentStore.DeleteAttachment(guid)
}
// Private
func (s *Server) registerAuthToken(token *model.AuthToken) {
s.authTokens = append(s.authTokens, *token)
}
func (s *Server) authenticateToken(token string) bool {
for _, t := range s.authTokens {
if t.SignedToken == token {
return true
}
}
return false
}