From de09aae98734ce3008fbc1b00ea9e58bd47c5131 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sat, 2 Feb 2019 11:15:21 +0800 Subject: [PATCH] add extras to message model --- api/message.go | 53 +++++++++++++-- api/message_test.go | 126 +++++++++++++++++++++++------------ api/stream/client.go | 4 +- api/stream/stream.go | 2 +- api/stream/stream_test.go | 76 ++++++++++----------- api/user.go | 18 ++--- docs/spec.json | 21 +++++- model/application.go | 4 +- model/message.go | 33 +++++++-- model/paging.go | 2 +- ui/src/tests/message.test.ts | 6 +- ui/src/types.ts | 5 ++ 12 files changed, 241 insertions(+), 109 deletions(-) diff --git a/api/message.go b/api/message.go index 478dcb1..88cd6d3 100644 --- a/api/message.go +++ b/api/message.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "errors" "strconv" "strings" @@ -30,7 +31,7 @@ var timeNow = time.Now // Notifier notifies when a new message was created. type Notifier interface { - Notify(userID uint, message *model.Message) + Notify(userID uint, message *model.MessageExternal) } // The MessageAPI provides handlers for managing messages. @@ -110,7 +111,7 @@ func buildWithPaging(ctx *gin.Context, paging *pagingParams, messages []*model.M } return &model.PagedMessages{ Paging: model.Paging{Size: len(useMessages), Limit: paging.Limit, Next: next, Since: since}, - Messages: useMessages, + Messages: toExternalMessages(useMessages), } } @@ -329,7 +330,7 @@ func (a *MessageAPI) DeleteMessage(ctx *gin.Context) { // schema: // $ref: "#/definitions/Error" func (a *MessageAPI) CreateMessage(ctx *gin.Context) { - message := model.Message{} + message := model.MessageExternal{} if err := ctx.Bind(&message); err == nil { application := a.DB.GetApplicationByToken(auth.GetTokenID(ctx)) message.ApplicationID = application.ID @@ -337,8 +338,48 @@ func (a *MessageAPI) CreateMessage(ctx *gin.Context) { message.Title = application.Name } message.Date = timeNow() - a.DB.CreateMessage(&message) - a.Notifier.Notify(auth.GetUserID(ctx), &message) - ctx.JSON(200, message) + msgInternal := toInternalMessage(&message) + a.DB.CreateMessage(msgInternal) + a.Notifier.Notify(auth.GetUserID(ctx), toExternalMessage(msgInternal)) + ctx.JSON(200, toExternalMessage(msgInternal)) } } + +func toInternalMessage(msg *model.MessageExternal) *model.Message { + res := &model.Message{ + ID: msg.ID, + ApplicationID: msg.ApplicationID, + Message: msg.Message, + Title: msg.Title, + Priority: msg.Priority, + Date: msg.Date, + } + if msg.Extras != nil { + res.Extras, _ = json.Marshal(msg.Extras) + } + return res +} + +func toExternalMessage(msg *model.Message) *model.MessageExternal { + res := &model.MessageExternal{ + ID: msg.ID, + ApplicationID: msg.ApplicationID, + Message: msg.Message, + Title: msg.Title, + Priority: msg.Priority, + Date: msg.Date, + } + if len(msg.Extras) != 0 { + res.Extras = make(map[string]interface{}) + json.Unmarshal(msg.Extras, &res.Extras) + } + return res +} + +func toExternalMessages(msg []*model.Message) []*model.MessageExternal { + res := make([]*model.MessageExternal, len(msg)) + for i := range msg { + res[i] = toExternalMessage(msg[i]) + } + return res +} diff --git a/api/message_test.go b/api/message_test.go index 73f06bb..70b7c09 100644 --- a/api/message_test.go +++ b/api/message_test.go @@ -22,11 +22,11 @@ func TestMessageSuite(t *testing.T) { type MessageSuite struct { suite.Suite - db *test.Database - a *MessageAPI - ctx *gin.Context - recorder *httptest.ResponseRecorder - notified bool + db *test.Database + a *MessageAPI + ctx *gin.Context + recorder *httptest.ResponseRecorder + notifiedMessage *model.MessageExternal } func (s *MessageSuite) BeforeTest(suiteName, testName string) { @@ -35,7 +35,7 @@ func (s *MessageSuite) BeforeTest(suiteName, testName string) { s.ctx, _ = gin.CreateTestContext(s.recorder) s.ctx.Request = httptest.NewRequest("GET", "/irrelevant", nil) s.db = test.NewDB(s.T()) - s.notified = false + s.notifiedMessage = nil s.a = &MessageAPI{DB: s.db, Notifier: s} } @@ -43,32 +43,39 @@ func (s *MessageSuite) AfterTest(string, string) { s.db.Close() } -func (s *MessageSuite) Notify(userID uint, msg *model.Message) { - s.notified = true +func (s *MessageSuite) Notify(userID uint, msg *model.MessageExternal) { + s.notifiedMessage = msg } func (s *MessageSuite) Test_ensureCorrectJsonRepresentation() { t, _ := time.Parse("2006/01/02", "2017/01/02") actual := &model.PagedMessages{ - Paging: model.Paging{Limit: 5, Since: 122, Size: 5, Next: "http://example.com/message?limit=5&since=122"}, - Messages: []*model.Message{{ID: 55, ApplicationID: 2, Message: "hi", Title: "hi", Date: t, Priority: 4}}, + Paging: model.Paging{Limit: 5, Since: 122, Size: 5, Next: "http://example.com/message?limit=5&since=122"}, + Messages: []*model.MessageExternal{{ID: 55, ApplicationID: 2, Message: "hi", Title: "hi", Date: t, Priority: 4, Extras: map[string]interface{}{ + "test::string": "string", + "test::array": []interface{}{1, 2, 3}, + "test::int": 1, + "test::float": 0.5, + }}}, } test.JSONEquals(s.T(), actual, `{"paging": {"limit":5, "since": 122, "size": 5, "next": "http://example.com/message?limit=5&since=122"}, - "messages": [{"id":55,"appid":2,"message":"hi","title":"hi","priority":4,"date":"2017-01-02T00:00:00Z"}]}`) + "messages": [{"id":55,"appid":2,"message":"hi","title":"hi","priority":4,"date":"2017-01-02T00:00:00Z","extras":{"test::string":"string","test::array":[1,2,3],"test::int":1,"test::float":0.5}}]}`) } func (s *MessageSuite) Test_GetMessages() { user := s.db.User(5) first := user.App(1).NewMessage(1) second := user.App(2).NewMessage(2) + firstExternal := toExternalMessage(&first) + secondExternal := toExternalMessage(&second) test.WithUser(s.ctx, 5) s.a.GetMessages(s.ctx) expected := &model.PagedMessages{ Paging: model.Paging{Limit: 100, Size: 2, Next: ""}, - Messages: []*model.Message{&second, &first}, + Messages: []*model.MessageExternal{secondExternal, firstExternal}, } test.BodyEquals(s.T(), expected, s.recorder) @@ -92,7 +99,7 @@ func (s *MessageSuite) Test_GetMessages_WithLimit_ReturnsNext() { // Since: entries with ids from 100 - 96 will be returned (5 entries) expected := &model.PagedMessages{ Paging: model.Paging{Limit: 5, Size: 5, Since: 96, Next: "http://example.com/messages?limit=5&since=96"}, - Messages: messages[:5], + Messages: toExternalMessages(messages[:5]), } test.BodyEquals(s.T(), expected, s.recorder) @@ -116,7 +123,7 @@ func (s *MessageSuite) Test_GetMessages_WithLimit_WithSince_ReturnsNext() { // Since: entries with ids from 54 - 42 will be returned (13 entries) expected := &model.PagedMessages{ Paging: model.Paging{Limit: 13, Size: 13, Since: 42, Next: "http://example.com/messages?limit=13&since=42"}, - Messages: messages[46 : 46+13], + Messages: toExternalMessages(messages[46 : 46+13]), } test.BodyEquals(s.T(), expected, s.recorder) } @@ -159,7 +166,7 @@ func (s *MessageSuite) Test_GetMessagesWithToken() { expected := &model.PagedMessages{ Paging: model.Paging{Limit: 100, Size: 1, Next: ""}, - Messages: []*model.Message{&msg}, + Messages: toExternalMessages([]*model.Message{&msg}), } test.BodyEquals(s.T(), expected, s.recorder) @@ -182,7 +189,7 @@ func (s *MessageSuite) Test_GetMessagesWithToken_WithLimit_ReturnsNext() { // Since: entries with ids from 100 - 92 will be returned (9 entries) expected := &model.PagedMessages{ Paging: model.Paging{Limit: 9, Size: 9, Since: 92, Next: "http://example.com/app/2/message?limit=9&since=92"}, - Messages: messages[:9], + Messages: toExternalMessages(messages[:9]), } test.BodyEquals(s.T(), expected, s.recorder) @@ -205,7 +212,7 @@ func (s *MessageSuite) Test_GetMessagesWithToken_WithLimit_WithSince_ReturnsNext // Since: entries with ids from 54 - 42 will be returned (13 entries) expected := &model.PagedMessages{ Paging: model.Paging{Limit: 13, Size: 13, Since: 42, Next: "http://example.com/app/2/message?limit=13&since=42"}, - Messages: messages[46 : 46+13], + Messages: toExternalMessages(messages[46 : 46+13]), } test.BodyEquals(s.T(), expected, s.recorder) } @@ -316,17 +323,17 @@ func (s *MessageSuite) Test_CreateMessage_onJson_allParams() { auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") s.db.User(4).AppWithToken(7, "app-token") - s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": 1}`)) + s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": 1}`)) s.ctx.Request.Header.Set("Content-Type", "application/json") s.a.CreateMessage(s.ctx) msgs := s.db.GetMessagesByApplication(7) - expected := &model.Message{ID: 1, ApplicationID: 7, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} + expected := &model.MessageExternal{ID: 1, ApplicationID: 7, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} assert.Len(s.T(), msgs, 1) - assert.Contains(s.T(), msgs, expected) + assert.Equal(s.T(), expected, toExternalMessage(msgs[0])) assert.Equal(s.T(), 200, s.recorder.Code) - assert.True(s.T(), s.notified) + assert.Equal(s.T(), expected, s.notifiedMessage) } func (s *MessageSuite) Test_CreateMessage_WithTitle() { @@ -336,38 +343,38 @@ func (s *MessageSuite) Test_CreateMessage_WithTitle() { auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") s.db.User(4).AppWithToken(5, "app-token") - s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle", "message": "mymessage"}`)) + s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage"}`)) s.ctx.Request.Header.Set("Content-Type", "application/json") s.a.CreateMessage(s.ctx) msgs := s.db.GetMessagesByApplication(5) - expected := &model.Message{ID: 1, ApplicationID: 5, Title: "mytitle", Message: "mymessage", Date: t} + expected := &model.MessageExternal{ID: 1, ApplicationID: 5, Title: "mytitle", Message: "mymessage", Date: t} assert.Len(s.T(), msgs, 1) - assert.Contains(s.T(), msgs, expected) + assert.Equal(s.T(), expected, toExternalMessage(msgs[0])) assert.Equal(s.T(), 200, s.recorder.Code) - assert.True(s.T(), s.notified) + assert.Equal(s.T(), expected, s.notifiedMessage) } func (s *MessageSuite) Test_CreateMessage_failWhenNoMessage() { auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") s.db.User(4).AppWithToken(1, "app-token") - s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle"}`)) + s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle"}`)) s.ctx.Request.Header.Set("Content-Type", "application/json") s.a.CreateMessage(s.ctx) assert.Empty(s.T(), s.db.GetMessagesByApplication(1)) assert.Equal(s.T(), 400, s.recorder.Code) - assert.False(s.T(), s.notified) + assert.Nil(s.T(), s.notifiedMessage) } func (s *MessageSuite) Test_CreateMessage_WithoutTitle() { auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name") - s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"message": "mymessage"}`)) + s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"message": "mymessage"}`)) s.ctx.Request.Header.Set("Content-Type", "application/json") s.a.CreateMessage(s.ctx) @@ -376,14 +383,14 @@ func (s *MessageSuite) Test_CreateMessage_WithoutTitle() { assert.Len(s.T(), msgs, 1) assert.Equal(s.T(), "Application name", msgs[0].Title) assert.Equal(s.T(), 200, s.recorder.Code) - assert.True(s.T(), s.notified) + assert.Equal(s.T(), "mymessage", s.notifiedMessage.Message) } func (s *MessageSuite) Test_CreateMessage_WithBlankTitle() { auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name") - s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"message": "mymessage", "title": " "}`)) + s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"message": "mymessage", "title": " "}`)) s.ctx.Request.Header.Set("Content-Type", "application/json") s.a.CreateMessage(s.ctx) @@ -392,20 +399,56 @@ func (s *MessageSuite) Test_CreateMessage_WithBlankTitle() { assert.Len(s.T(), msgs, 1) assert.Equal(s.T(), "Application name", msgs[0].Title) assert.Equal(s.T(), 200, s.recorder.Code) - assert.True(s.T(), s.notified) + assert.Equal(s.T(), "mymessage", msgs[0].Message) +} +func (s *MessageSuite) Test_CreateMessage_WithExtras() { + auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") + s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name") + + t, _ := time.Parse("2006/01/02", "2017/01/02") + timeNow = func() time.Time { return t } + defer func() { timeNow = time.Now }() + + s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"message": "mymessage", "title": "msg with extras", "extras": {"gotify::test":{"int":1,"float":0.5,"string":"test","array":[1,2,3]}}}`)) + s.ctx.Request.Header.Set("Content-Type", "application/json") + + s.a.CreateMessage(s.ctx) + + msgs := s.db.GetMessagesByApplication(8) + expected := &model.MessageExternal{ + ID: 1, + ApplicationID: 8, + Message: "mymessage", + Title: "msg with extras", + Date: t, + Extras: map[string]interface{}{ + "gotify::test": map[string]interface{}{ + "string": "test", + "array": []interface{}{float64(1), float64(2), float64(3)}, + "int": float64(1), + "float": float64(0.5), + }, + }, + } + assert.Len(s.T(), msgs, 1) + + assert.Equal(s.T(), expected, toExternalMessage(msgs[0])) + + assert.Equal(s.T(), 200, s.recorder.Code) + assert.Equal(s.T(), uint(1), s.notifiedMessage.ID) } func (s *MessageSuite) Test_CreateMessage_failWhenPriorityNotNumber() { auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") s.db.User(4).AppWithToken(8, "app-token") - s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": "asd"}`)) + s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": "asd"}`)) s.ctx.Request.Header.Set("Content-Type", "application/json") s.a.CreateMessage(s.ctx) assert.Equal(s.T(), 400, s.recorder.Code) - assert.False(s.T(), s.notified) + assert.Nil(s.T(), s.notifiedMessage) assert.Empty(s.T(), s.db.GetMessagesByApplication(1)) } @@ -417,20 +460,19 @@ func (s *MessageSuite) Test_CreateMessage_onQueryData() { timeNow = func() time.Time { return t } defer func() { timeNow = time.Now }() - s.ctx.Request = httptest.NewRequest("POST", "/token?title=mytitle&message=mymessage&priority=1", nil) + s.ctx.Request = httptest.NewRequest("POST", "/message?title=mytitle&message=mymessage&priority=1", nil) s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") s.a.CreateMessage(s.ctx) - expected := &model.Message{ID: 1, ApplicationID: 2, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} + expected := &model.MessageExternal{ID: 1, ApplicationID: 2, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} msgs := s.db.GetMessagesByApplication(2) assert.Len(s.T(), msgs, 1) - assert.Contains(s.T(), msgs, expected) + assert.Equal(s.T(), expected, toExternalMessage(msgs[0])) assert.Equal(s.T(), 200, s.recorder.Code) - assert.True(s.T(), s.notified) + assert.Equal(s.T(), uint(1), s.notifiedMessage.ID) } - func (s *MessageSuite) Test_CreateMessage_onFormData() { auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") s.db.User(4).AppWithToken(99, "app-token") @@ -439,17 +481,17 @@ func (s *MessageSuite) Test_CreateMessage_onFormData() { timeNow = func() time.Time { return t } defer func() { timeNow = time.Now }() - s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader("title=mytitle&message=mymessage&priority=1")) + s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`title=mytitle&message=mymessage&priority=1`)) s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") s.a.CreateMessage(s.ctx) - expected := &model.Message{ID: 1, ApplicationID: 99, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} + expected := &model.MessageExternal{ID: 1, ApplicationID: 99, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} msgs := s.db.GetMessagesByApplication(99) assert.Len(s.T(), msgs, 1) - assert.Contains(s.T(), msgs, expected) + assert.Equal(s.T(), expected, toExternalMessage(msgs[0])) assert.Equal(s.T(), 200, s.recorder.Code) - assert.True(s.T(), s.notified) + assert.Equal(s.T(), uint(1), s.notifiedMessage.ID) } func (s *MessageSuite) withURL(scheme, host, path, query string) { diff --git a/api/stream/client.go b/api/stream/client.go index dc8d222..30dd69b 100644 --- a/api/stream/client.go +++ b/api/stream/client.go @@ -22,7 +22,7 @@ var writeJSON = func(conn *websocket.Conn, v interface{}) error { type client struct { conn *websocket.Conn onClose func(*client) - write chan *model.Message + write chan *model.MessageExternal userID uint token string once once @@ -31,7 +31,7 @@ type client struct { func newClient(conn *websocket.Conn, userID uint, token string, onClose func(*client)) *client { return &client{ conn: conn, - write: make(chan *model.Message, 1), + write: make(chan *model.MessageExternal, 1), userID: userID, token: token, onClose: onClose, diff --git a/api/stream/stream.go b/api/stream/stream.go index 5a149d4..42b0d75 100644 --- a/api/stream/stream.go +++ b/api/stream/stream.go @@ -65,7 +65,7 @@ func (a *API) NotifyDeletedClient(userID uint, token string) { } // Notify notifies the clients with the given userID that a new messages was created. -func (a *API) Notify(userID uint, msg *model.Message) { +func (a *API) Notify(userID uint, msg *model.MessageExternal) { a.lock.RLock() defer a.lock.RUnlock() if clients, ok := a.clients[userID]; ok { diff --git a/api/stream/stream_test.go b/api/stream/stream_test.go index 9118b3d..21f9551 100644 --- a/api/stream/stream_test.go +++ b/api/stream/stream_test.go @@ -61,7 +61,7 @@ func TestWriteMessageFails(t *testing.T) { clients := clients(api, 1) assert.NotEmpty(t, clients) - api.Notify(1, &model.Message{Message: "HI"}) + api.Notify(1, &model.MessageExternal{Message: "HI"}) user.expectNoMessage() } @@ -94,7 +94,7 @@ func TestWritePingFails(t *testing.T) { time.Sleep(5 * time.Second) // waiting for ping - api.Notify(1, &model.Message{Message: "HI"}) + api.Notify(1, &model.MessageExternal{Message: "HI"}) user.expectNoMessage() } @@ -130,8 +130,8 @@ func TestPing(t *testing.T) { } expectNoMessage(user) - api.Notify(1, &model.Message{Message: "HI"}) - user.expectMessage(&model.Message{Message: "HI"}) + api.Notify(1, &model.MessageExternal{Message: "HI"}) + user.expectMessage(&model.MessageExternal{Message: "HI"}) } func TestCloseClientOnNotReading(t *testing.T) { @@ -169,8 +169,8 @@ func TestMessageDirectlyAfterConnect(t *testing.T) { defer user.conn.Close() // the server may take some time to register the client time.Sleep(100 * time.Millisecond) - api.Notify(1, &model.Message{Message: "msg"}) - user.expectMessage(&model.Message{Message: "msg"}) + api.Notify(1, &model.MessageExternal{Message: "msg"}) + user.expectMessage(&model.MessageExternal{Message: "msg"}) } func TestDeleteClientShouldCloseConnection(t *testing.T) { @@ -186,12 +186,12 @@ func TestDeleteClientShouldCloseConnection(t *testing.T) { defer user.conn.Close() // the server may take some time to register the client time.Sleep(100 * time.Millisecond) - api.Notify(1, &model.Message{Message: "msg"}) - user.expectMessage(&model.Message{Message: "msg"}) + api.Notify(1, &model.MessageExternal{Message: "msg"}) + user.expectMessage(&model.MessageExternal{Message: "msg"}) api.NotifyDeletedClient(1, "customtoken") - api.Notify(1, &model.Message{Message: "msg"}) + api.Notify(1, &model.MessageExternal{Message: "msg"}) user.expectNoMessage() } @@ -233,28 +233,28 @@ func TestDeleteMultipleClients(t *testing.T) { // the server may take some time to register the client time.Sleep(100 * time.Millisecond) - api.Notify(1, &model.Message{ID: 4, Message: "there"}) - expectMessage(&model.Message{ID: 4, Message: "there"}, userOne...) + api.Notify(1, &model.MessageExternal{ID: 4, Message: "there"}) + expectMessage(&model.MessageExternal{ID: 4, Message: "there"}, userOne...) expectNoMessage(userTwo...) expectNoMessage(userThree...) api.NotifyDeletedClient(1, "1-2") - api.Notify(1, &model.Message{ID: 2, Message: "there"}) - expectMessage(&model.Message{ID: 2, Message: "there"}, userOneIPhone, userOneOther) + api.Notify(1, &model.MessageExternal{ID: 2, Message: "there"}) + expectMessage(&model.MessageExternal{ID: 2, Message: "there"}, userOneIPhone, userOneOther) expectNoMessage(userOneBrowser, userOneAndroid) expectNoMessage(userThree...) expectNoMessage(userTwo...) - api.Notify(2, &model.Message{ID: 2, Message: "there"}) + api.Notify(2, &model.MessageExternal{ID: 2, Message: "there"}) expectNoMessage(userOne...) - expectMessage(&model.Message{ID: 2, Message: "there"}, userTwo...) + expectMessage(&model.MessageExternal{ID: 2, Message: "there"}, userTwo...) expectNoMessage(userThree...) - api.Notify(3, &model.Message{ID: 5, Message: "there"}) + api.Notify(3, &model.MessageExternal{ID: 5, Message: "there"}) expectNoMessage(userOne...) expectNoMessage(userTwo...) - expectMessage(&model.Message{ID: 5, Message: "there"}, userThree...) + expectMessage(&model.MessageExternal{ID: 5, Message: "there"}, userThree...) api.Close() } @@ -297,27 +297,27 @@ func TestDeleteUser(t *testing.T) { // the server may take some time to register the client time.Sleep(100 * time.Millisecond) - api.Notify(1, &model.Message{ID: 4, Message: "there"}) - expectMessage(&model.Message{ID: 4, Message: "there"}, userOne...) + api.Notify(1, &model.MessageExternal{ID: 4, Message: "there"}) + expectMessage(&model.MessageExternal{ID: 4, Message: "there"}, userOne...) expectNoMessage(userTwo...) expectNoMessage(userThree...) api.NotifyDeletedUser(1) - api.Notify(1, &model.Message{ID: 2, Message: "there"}) + api.Notify(1, &model.MessageExternal{ID: 2, Message: "there"}) expectNoMessage(userOne...) expectNoMessage(userThree...) expectNoMessage(userTwo...) - api.Notify(2, &model.Message{ID: 2, Message: "there"}) + api.Notify(2, &model.MessageExternal{ID: 2, Message: "there"}) expectNoMessage(userOne...) - expectMessage(&model.Message{ID: 2, Message: "there"}, userTwo...) + expectMessage(&model.MessageExternal{ID: 2, Message: "there"}, userTwo...) expectNoMessage(userThree...) - api.Notify(3, &model.Message{ID: 5, Message: "there"}) + api.Notify(3, &model.MessageExternal{ID: 5, Message: "there"}) expectNoMessage(userOne...) expectNoMessage(userTwo...) - expectMessage(&model.Message{ID: 5, Message: "there"}, userThree...) + expectMessage(&model.MessageExternal{ID: 5, Message: "there"}, userThree...) api.Close() } @@ -362,15 +362,15 @@ func TestMultipleClients(t *testing.T) { expectNoMessage(userTwo...) expectNoMessage(userThree...) - api.Notify(1, &model.Message{ID: 1, Message: "hello"}) + api.Notify(1, &model.MessageExternal{ID: 1, Message: "hello"}) time.Sleep(1 * time.Second) - expectMessage(&model.Message{ID: 1, Message: "hello"}, userOne...) + expectMessage(&model.MessageExternal{ID: 1, Message: "hello"}, userOne...) expectNoMessage(userTwo...) expectNoMessage(userThree...) - api.Notify(2, &model.Message{ID: 2, Message: "there"}) + api.Notify(2, &model.MessageExternal{ID: 2, Message: "there"}) expectNoMessage(userOne...) - expectMessage(&model.Message{ID: 2, Message: "there"}, userTwo...) + expectMessage(&model.MessageExternal{ID: 2, Message: "there"}, userTwo...) expectNoMessage(userThree...) userOneIPhone.conn.Close() @@ -379,21 +379,21 @@ func TestMultipleClients(t *testing.T) { expectNoMessage(userTwo...) expectNoMessage(userThree...) - api.Notify(1, &model.Message{ID: 3, Message: "how"}) - expectMessage(&model.Message{ID: 3, Message: "how"}, userOneAndroid, userOneBrowser) + api.Notify(1, &model.MessageExternal{ID: 3, Message: "how"}) + expectMessage(&model.MessageExternal{ID: 3, Message: "how"}, userOneAndroid, userOneBrowser) expectNoMessage(userOneIPhone) expectNoMessage(userTwo...) expectNoMessage(userThree...) - api.Notify(2, &model.Message{ID: 4, Message: "are"}) + api.Notify(2, &model.MessageExternal{ID: 4, Message: "are"}) expectNoMessage(userOne...) - expectMessage(&model.Message{ID: 4, Message: "are"}, userTwo...) + expectMessage(&model.MessageExternal{ID: 4, Message: "are"}, userTwo...) expectNoMessage(userThree...) api.Close() - api.Notify(2, &model.Message{ID: 5, Message: "you"}) + api.Notify(2, &model.MessageExternal{ID: 5, Message: "you"}) expectNoMessage(userOne...) expectNoMessage(userTwo...) @@ -481,7 +481,7 @@ func startReading(client *testingClient) { return } - actual := &model.Message{} + actual := &model.MessageExternal{} json.NewDecoder(bytes.NewBuffer(payload)).Decode(actual) client.readMessage <- *actual } @@ -492,18 +492,18 @@ func createClient(t *testing.T, url string) *testingClient { ws, _, err := websocket.DefaultDialer.Dial(url, nil) assert.Nil(t, err) - readMessages := make(chan model.Message) + readMessages := make(chan model.MessageExternal) return &testingClient{conn: ws, readMessage: readMessages, t: t} } type testingClient struct { conn *websocket.Conn - readMessage chan model.Message + readMessage chan model.MessageExternal t *testing.T } -func (c *testingClient) expectMessage(expected *model.Message) { +func (c *testingClient) expectMessage(expected *model.MessageExternal) { select { case <-time.After(50 * time.Millisecond): assert.Fail(c.t, "Expected message but none was send :(") @@ -512,7 +512,7 @@ func (c *testingClient) expectMessage(expected *model.Message) { } } -func expectMessage(expected *model.Message, clients ...*testingClient) { +func expectMessage(expected *model.MessageExternal, clients ...*testingClient) { for _, client := range clients { client.expectMessage(expected) } diff --git a/api/user.go b/api/user.go index 88e393a..e609cb1 100644 --- a/api/user.go +++ b/api/user.go @@ -54,7 +54,7 @@ func (a *UserAPI) GetUsers(ctx *gin.Context) { var resp []*model.UserExternal for _, user := range users { - resp = append(resp, toExternal(user)) + resp = append(resp, toExternalUser(user)) } ctx.JSON(200, resp) @@ -83,7 +83,7 @@ func (a *UserAPI) GetUsers(ctx *gin.Context) { // $ref: "#/definitions/Error" func (a *UserAPI) GetCurrentUser(ctx *gin.Context) { user := a.DB.GetUserByID(auth.GetUserID(ctx)) - ctx.JSON(200, toExternal(user)) + ctx.JSON(200, toExternalUser(user)) } // CreateUser creates a user @@ -122,10 +122,10 @@ func (a *UserAPI) GetCurrentUser(ctx *gin.Context) { func (a *UserAPI) CreateUser(ctx *gin.Context) { user := model.UserExternalWithPass{} if err := ctx.Bind(&user); err == nil { - internal := a.toInternal(&user, []byte{}) + internal := a.toInternalUser(&user, []byte{}) if a.DB.GetUserByName(internal.Name) == nil { a.DB.CreateUser(internal) - ctx.JSON(200, toExternal(internal)) + ctx.JSON(200, toExternalUser(internal)) } else { ctx.AbortWithError(400, errors.New("username already exists")) } @@ -171,7 +171,7 @@ func (a *UserAPI) CreateUser(ctx *gin.Context) { func (a *UserAPI) GetUserByID(ctx *gin.Context) { withID(ctx, "id", func(id uint) { if user := a.DB.GetUserByID(uint(id)); user != nil { - ctx.JSON(200, toExternal(user)) + ctx.JSON(200, toExternalUser(user)) } else { ctx.AbortWithError(404, errors.New("user does not exist")) } @@ -309,10 +309,10 @@ func (a *UserAPI) UpdateUserByID(ctx *gin.Context) { var user *model.UserExternalWithPass if err := ctx.Bind(&user); err == nil { if oldUser := a.DB.GetUserByID(id); oldUser != nil { - internal := a.toInternal(user, oldUser.Pass) + internal := a.toInternalUser(user, oldUser.Pass) internal.ID = id a.DB.UpdateUser(internal) - ctx.JSON(200, toExternal(internal)) + ctx.JSON(200, toExternalUser(internal)) } else { ctx.AbortWithError(404, errors.New("user does not exist")) } @@ -320,7 +320,7 @@ func (a *UserAPI) UpdateUserByID(ctx *gin.Context) { }) } -func (a *UserAPI) toInternal(response *model.UserExternalWithPass, pw []byte) *model.User { +func (a *UserAPI) toInternalUser(response *model.UserExternalWithPass, pw []byte) *model.User { user := &model.User{ Name: response.Name, Admin: response.Admin, @@ -333,7 +333,7 @@ func (a *UserAPI) toInternal(response *model.UserExternalWithPass, pw []byte) *m return user } -func toExternal(internal *model.User) *model.UserExternal { +func toExternalUser(internal *model.User) *model.UserExternal { return &model.UserExternal{ Name: internal.Name, Admin: internal.Admin, diff --git a/docs/spec.json b/docs/spec.json index 4d1d44c..a6701e7 100644 --- a/docs/spec.json +++ b/docs/spec.json @@ -1490,9 +1490,9 @@ "x-go-package": "github.com/gotify/server/model" }, "Message": { - "description": "The Message holds information about a message which was sent by an Application.", + "description": "The MessageExternal holds information about a message which was sent by an Application.", "type": "object", - "title": "Message Model", + "title": "MessageExternal Model", "required": [ "id", "appid", @@ -1516,6 +1516,22 @@ "readOnly": true, "example": "2018-02-27T19:36:10.5045044+01:00" }, + "extras": { + "description": "The extra data sent along the message.\n\nThe extra fields are stored in a key-value scheme. Only accepted in CreateMessage requests with application/json content-type.\n\nThe keys should be in the following format: \u0026lt;top-namespace\u0026gt;::[\u0026lt;sub-namespace\u0026gt;::]\u0026lt;action\u0026gt;\n\nThese namespaces are reserved and might be used in the official clients: gotify android ios web server client. Do not use them for other purposes.", + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Extras", + "example": { + "home::appliances::lighting::on": { + "brightness": 15 + }, + "home::appliances::thermostat::change_temperature": { + "temperature": 23 + } + } + }, "id": { "description": "The message id.", "type": "integer", @@ -1544,6 +1560,7 @@ "example": "Backup" } }, + "x-go-name": "MessageExternal", "x-go-package": "github.com/gotify/server/model" }, "PagedMessages": { diff --git a/model/application.go b/model/application.go index 844570d..e8c4e4c 100644 --- a/model/application.go +++ b/model/application.go @@ -34,6 +34,6 @@ type Application struct { // read only: true // required: true // example: https://example.com/image.jpeg - Image string `json:"image"` - Messages []Message `json:"-"` + Image string `json:"image"` + Messages []MessageExternal `json:"-"` } diff --git a/model/message.go b/model/message.go index 2821e20..7c7fb48 100644 --- a/model/message.go +++ b/model/message.go @@ -1,19 +1,32 @@ package model -import "time" +import ( + "time" +) -// Message Model +// Message holds information about a message +type Message struct { + ID uint `gorm:"AUTO_INCREMENT;primary_key;index"` + ApplicationID uint + Message string + Title string + Priority int + Extras []byte + Date time.Time +} + +// MessageExternal Model // -// The Message holds information about a message which was sent by an Application. +// The MessageExternal holds information about a message which was sent by an Application. // // swagger:model Message -type Message struct { +type MessageExternal struct { // The message id. // // read only: true // required: true // example: 25 - ID uint `gorm:"AUTO_INCREMENT;primary_key;index" json:"id"` + ID uint `json:"id"` // The application id that send this message. // // read only: true @@ -33,6 +46,16 @@ type Message struct { // // example: 2 Priority int `form:"priority" query:"priority" json:"priority"` + // The extra data sent along the message. + // + // The extra fields are stored in a key-value scheme. Only accepted in CreateMessage requests with application/json content-type. + // + // The keys should be in the following format: <top-namespace>::[<sub-namespace>::]<action> + // + // These namespaces are reserved and might be used in the official clients: gotify android ios web server client. Do not use them for other purposes. + // + // example: {"home::appliances::thermostat::change_temperature":{"temperature":23},"home::appliances::lighting::on":{"brightness":15}} + Extras map[string]interface{} `form:"-" query:"-" json:"extras,omitempty"` // The date the message was created. // // read only: true diff --git a/model/paging.go b/model/paging.go index f13d041..855c6cd 100644 --- a/model/paging.go +++ b/model/paging.go @@ -50,5 +50,5 @@ type PagedMessages struct { // // read only: true // required: true - Messages []*Message `json:"messages"` + Messages []*MessageExternal `json:"messages"` } diff --git a/ui/src/tests/message.test.ts b/ui/src/tests/message.test.ts index 7e76c1b..9b3f5b3 100644 --- a/ui/src/tests/message.test.ts +++ b/ui/src/tests/message.test.ts @@ -102,7 +102,11 @@ describe('Messages', () => { } return result; }; - const m = (title: string, message: string) => ({title, message}); + const m = (title: string, message: string, extras?: IMessageExtras) => ({ + title, + message, + extras, + }); const windows1 = m('Login', 'User jmattheis logged in.'); const windows2 = m('Shutdown', 'Windows will be shut down.'); diff --git a/ui/src/types.ts b/ui/src/types.ts index c8ef55f..23be733 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -20,6 +20,11 @@ interface IMessage { priority: number; date: string; image?: string; + extras?: IMessageExtras; +} + +interface IMessageExtras { + [key: string]: any; // tslint:disable-line no-any } interface IPagedMessages {