diff --git a/scnserver/README.md b/scnserver/README.md index 62f9c15..a438efc 100644 --- a/scnserver/README.md +++ b/scnserver/README.md @@ -20,8 +20,10 @@ - Pagination for ListChannels / ListSubscriptions / ListClients / ListChannelSubscriptions / ListUserSubscriptions - cannot open sqlite in dbbrowsr (cannot parse schema?) + -> https://github.com/sqlitebrowser/sqlitebrowser/issues/292 -> https://github.com/sqlitebrowser/sqlitebrowser/issues/29266 - (?) use str-ids (also prevents wrong-joins) -> see psycho + -> how does it work with existing data? (do i care, there are only 2 active users... (are there?)) - error logging as goroutine, get sall errors via channel, (channel buffered - nonblocking send, second channel that gets a message when sender failed ) @@ -34,9 +36,13 @@ -> logs and request-logging into their own sqlite files (sqlite-files are prepped) + - /send endpoint should be compatible with the [ webhook ] notifier of uptime-kuma + (or add another /kuma endpoint) + -> https://webhook.site/ + ------------------------------------------------------------------------------------------------------------------------------- - - in my script: use (backupname || hostname) for sendername + - in my script: use `srvname` for sendername ------------------------------------------------------------------------------------------------------------------------------- @@ -46,4 +52,7 @@ - (?) "login" on website and list/search/filter messages - - (?) make channels deleteable (soft-delete) (what do with messages in channel?) \ No newline at end of file + - (?) make channels deleteable (soft-delete) (what do with messages in channel?) + + - (?) desktop client for notifications + diff --git a/scnserver/api/apierr/enums.go b/scnserver/api/apierr/enums.go index 4f5d47e..a981cf2 100644 --- a/scnserver/api/apierr/enums.go +++ b/scnserver/api/apierr/enums.go @@ -21,14 +21,15 @@ const ( INVALID_BODY_PARAM APIError = 1161 INVALID_ENUM_VALUE APIError = 1171 - NO_TITLE APIError = 1201 - TITLE_TOO_LONG APIError = 1202 - CONTENT_TOO_LONG APIError = 1203 - USR_MSG_ID_TOO_LONG APIError = 1204 - TIMESTAMP_OUT_OF_RANGE APIError = 1205 - SENDERNAME_TOO_LONG APIError = 1206 - CHANNEL_TOO_LONG APIError = 1207 - CHANNEL_NAME_WOULD_CHANGE APIError = 1207 + NO_TITLE APIError = 1201 + TITLE_TOO_LONG APIError = 1202 + CONTENT_TOO_LONG APIError = 1203 + USR_MSG_ID_TOO_LONG APIError = 1204 + TIMESTAMP_OUT_OF_RANGE APIError = 1205 + SENDERNAME_TOO_LONG APIError = 1206 + CHANNEL_TOO_LONG APIError = 1207 + CHANNEL_DESCRIPTION_TOO_LONG APIError = 1208 + CHANNEL_NAME_WOULD_CHANGE APIError = 1251 USER_NOT_FOUND APIError = 1301 CLIENT_NOT_FOUND APIError = 1302 diff --git a/scnserver/api/handler/api.go b/scnserver/api/handler/api.go index 9342854..72c5a1b 100644 --- a/scnserver/api/handler/api.go +++ b/scnserver/api/handler/api.go @@ -751,6 +751,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { RefreshSubscribeKey *bool `json:"subscribe_key"` RefreshSendKey *bool `json:"send_key"` DisplayName *string `json:"display_name"` + DescriptionName *string `json:"description_name"` } var u uri @@ -773,6 +774,14 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } + user, err := h.database.GetUser(ctx, u.UserID) + if err == sql.ErrNoRows { + return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) + } + if langext.Coalesce(b.RefreshSendKey, false) { newkey := h.app.GenerateRandomAuthKey() @@ -800,6 +809,10 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_WOULD_CHANGE, "Cannot substantially change the channel name", err) } + if len(newDisplayName) > user.MaxChannelNameLength() { + return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) + } + err := h.database.UpdateChannelDisplayName(ctx, u.ChannelID, newDisplayName) if err != nil { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) @@ -807,6 +820,24 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { } + if b.DescriptionName != nil { + + var descName *string = nil + if strings.TrimSpace(*b.DescriptionName) != "" { + descName = langext.Ptr(strings.TrimSpace(*b.DescriptionName)) + } + + if descName != nil && len(*descName) > user.MaxChannelDescriptionNameLength() { + return ginresp.APIError(g, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG, fmt.Sprintf("Channel-Description too long (max %d characters)", user.MaxChannelNameLength()), nil) + } + + err := h.database.UpdateChannelDescriptionName(ctx, u.ChannelID, descName) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) + } + + } + channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) if err != nil { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) channel", err) diff --git a/scnserver/db/impl/primary/channels.go b/scnserver/db/impl/primary/channels.go index abfbfca..49a3a65 100644 --- a/scnserver/db/impl/primary/channels.go +++ b/scnserver/db/impl/primary/channels.go @@ -289,3 +289,20 @@ func (db *Database) UpdateChannelDisplayName(ctx TxContext, channelid models.Cha return nil } + +func (db *Database) UpdateChannelDescriptionName(ctx TxContext, channelid models.ChannelID, descname *string) error { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return err + } + + _, err = tx.Exec(ctx, "UPDATE channels SET description_name = :nam WHERE channel_id = :cid", sq.PP{ + "nam": descname, + "cid": channelid, + }) + if err != nil { + return err + } + + return nil +} diff --git a/scnserver/db/impl/primary/schema/schema_3.ddl b/scnserver/db/impl/primary/schema/schema_3.ddl index 1d67bb3..b2ddddf 100644 --- a/scnserver/db/impl/primary/schema/schema_3.ddl +++ b/scnserver/db/impl/primary/schema/schema_3.ddl @@ -48,6 +48,7 @@ CREATE TABLE channels internal_name TEXT NOT NULL, display_name TEXT NOT NULL, + description_name TEXT NULL, subscribe_key TEXT NOT NULL, send_key TEXT NOT NULL, diff --git a/scnserver/models/channel.go b/scnserver/models/channel.go index 5da738b..e6a3d37 100644 --- a/scnserver/models/channel.go +++ b/scnserver/models/channel.go @@ -12,6 +12,7 @@ type Channel struct { OwnerUserID UserID InternalName string DisplayName string + DescriptionName *string SubscribeKey string SendKey string TimestampCreated time.Time @@ -25,6 +26,7 @@ func (c Channel) JSON(includeKey bool) ChannelJSON { OwnerUserID: c.OwnerUserID, InternalName: c.InternalName, DisplayName: c.DisplayName, + DescriptionName: c.DescriptionName, SubscribeKey: langext.Conditional(includeKey, langext.Ptr(c.SubscribeKey), nil), SendKey: langext.Conditional(includeKey, langext.Ptr(c.SendKey), nil), TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano), @@ -61,6 +63,7 @@ type ChannelJSON struct { OwnerUserID UserID `json:"owner_user_id"` InternalName string `json:"internal_name"` DisplayName string `json:"display_name"` + DescriptionName *string `json:"description_name"` SubscribeKey *string `json:"subscribe_key"` // can be nil, depending on endpoint SendKey *string `json:"send_key"` // can be nil, depending on endpoint TimestampCreated string `json:"timestamp_created"` @@ -78,6 +81,7 @@ type ChannelDB struct { OwnerUserID UserID `db:"owner_user_id"` InternalName string `db:"internal_name"` DisplayName string `db:"display_name"` + DescriptionName *string `db:"description_name"` SubscribeKey string `db:"subscribe_key"` SendKey string `db:"send_key"` TimestampCreated int64 `db:"timestamp_created"` @@ -92,6 +96,7 @@ func (c ChannelDB) Model() Channel { OwnerUserID: c.OwnerUserID, InternalName: c.InternalName, DisplayName: c.DisplayName, + DescriptionName: c.DescriptionName, SubscribeKey: c.SubscribeKey, SendKey: c.SendKey, TimestampCreated: time.UnixMilli(c.TimestampCreated), diff --git a/scnserver/models/delivery.go b/scnserver/models/delivery.go index 7b66d62..5ee71e2 100644 --- a/scnserver/models/delivery.go +++ b/scnserver/models/delivery.go @@ -53,7 +53,7 @@ type DeliveryJSON struct { ReceiverUserID UserID `json:"receiver_user_id"` ReceiverClientID ClientID `json:"receiver_client_id"` TimestampCreated string `json:"timestamp_created"` - TimestampFinalized *string `json:"tiestamp_finalized"` + TimestampFinalized *string `json:"timestamp_finalized"` Status DeliveryStatus `json:"status"` RetryCount int `json:"retry_count"` NextDelivery *string `json:"next_delivery"` @@ -66,7 +66,7 @@ type DeliveryDB struct { ReceiverUserID UserID `db:"receiver_user_id"` ReceiverClientID ClientID `db:"receiver_client_id"` TimestampCreated int64 `db:"timestamp_created"` - TimestampFinalized *int64 `db:"tiestamp_finalized"` + TimestampFinalized *int64 `db:"timestamp_finalized"` Status DeliveryStatus `db:"status"` RetryCount int `db:"retry_count"` NextDelivery *int64 `db:"next_delivery"` diff --git a/scnserver/models/user.go b/scnserver/models/user.go index 4ae1437..45462d0 100644 --- a/scnserver/models/user.go +++ b/scnserver/models/user.go @@ -95,6 +95,10 @@ func (u User) MaxChannelNameLength() int { return 120 } +func (u User) MaxChannelDescriptionNameLength() int { + return 300 +} + func (u User) MaxSenderName() int { return 120 } diff --git a/scnserver/swagger/swagger.json b/scnserver/swagger/swagger.json index 50f319e..f1d5eaa 100644 --- a/scnserver/swagger/swagger.json +++ b/scnserver/swagger/swagger.json @@ -2844,6 +2844,9 @@ "channel_id": { "type": "integer" }, + "description_name": { + "type": "string" + }, "display_name": { "type": "string" }, diff --git a/scnserver/swagger/swagger.yaml b/scnserver/swagger/swagger.yaml index fd399fb..e9fb753 100644 --- a/scnserver/swagger/swagger.yaml +++ b/scnserver/swagger/swagger.yaml @@ -340,6 +340,8 @@ definitions: properties: channel_id: type: integer + description_name: + type: string display_name: type: string internal_name: diff --git a/scnserver/test/channel_test.go b/scnserver/test/channel_test.go index 4a8b9ff..9beb1f6 100644 --- a/scnserver/test/channel_test.go +++ b/scnserver/test/channel_test.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" + "strings" "testing" ) @@ -360,4 +361,135 @@ func TestListChannelsAll(t *testing.T) { } } +func TestChannelUpdate(t *testing.T) { + _, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + "agent_model": "DUMMY_PHONE", + "agent_version": "4X", + "client_type": "ANDROID", + "fcm_token": "DUMMY_FCM", + }) + + uid := int(r0["user_id"].(float64)) + admintok := r0["admin_key"].(string) + + type chanlist struct { + Channels []gin.H `json:"channels"` + } + + { + clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) + tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "display_name") + tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "internal_name") + } + + chan0 := tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{ + "name": "server-alerts", + }) + chanid := fmt.Sprintf("%v", chan0["channel_id"]) + + { + clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) + tt.AssertMappedSet(t, "channels", []string{"server-alerts"}, clist.Channels, "display_name") + tt.AssertMappedSet(t, "channels", []string{"server-alerts"}, clist.Channels, "internal_name") + tt.AssertEqual(t, "channels.descr", nil, clist.Channels[0]["description_name"]) + } + + { + chan1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid)) + tt.AssertEqual(t, "channels.display_name", "server-alerts", chan1["display_name"]) + tt.AssertEqual(t, "channels.internal_name", "server-alerts", chan1["internal_name"]) + tt.AssertEqual(t, "channels.description_name", nil, chan1["description_name"]) + tt.AssertEqual(t, "channels.subscribe_key", chan0["subscribe_key"], chan1["subscribe_key"]) + tt.AssertEqual(t, "channels.send_key", chan0["send_key"], chan1["send_key"]) + } + + // [1] update display_name + + tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid), gin.H{ + "display_name": "SERVER-ALERTS", + }) + + { + chan1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid)) + tt.AssertEqual(t, "channels.display_name", "SERVER-ALERTS", chan1["display_name"]) + tt.AssertEqual(t, "channels.internal_name", "server-alerts", chan1["internal_name"]) + tt.AssertEqual(t, "channels.description_name", nil, chan1["description_name"]) + tt.AssertEqual(t, "channels.subscribe_key", chan0["subscribe_key"], chan1["subscribe_key"]) + tt.AssertEqual(t, "channels.send_key", chan0["send_key"], chan1["send_key"]) + } + + // [2] fail to update display_name + + tt.RequestAuthPatchShouldFail(t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid), gin.H{ + "display_name": "SERVER-ALERTS2", + }, 400, apierr.CHANNEL_NAME_WOULD_CHANGE) + + // [3] renew subscribe_key + + tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid), gin.H{ + "subscribe_key": true, + }) + + { + chan1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid)) + tt.AssertNotEqual(t, "channels.subscribe_key", chan0["subscribe_key"], chan1["subscribe_key"]) + tt.AssertEqual(t, "channels.send_key", chan0["send_key"], chan1["send_key"]) + } + + // [4] renew send_key + + tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid), gin.H{ + "send_key": true, + }) + + { + chan1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid)) + tt.AssertNotEqual(t, "channels.subscribe_key", chan0["subscribe_key"], chan1["subscribe_key"]) + tt.AssertNotEqual(t, "channels.send_key", chan0["send_key"], chan1["send_key"]) + } + + // [5] update description_name + + tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid), gin.H{ + "description_name": "hello World", + }) + + { + chan1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid)) + tt.AssertEqual(t, "channels.description_name", "hello World", chan1["description_name"]) + } + + // [6] update description_name + + tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid), gin.H{ + "description_name": " AXXhello World9 ", + }) + + { + chan1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid)) + tt.AssertEqual(t, "channels.description_name", "AXXhello World9", chan1["description_name"]) + } + + // [7] clear description_name + + tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid), gin.H{ + "description_name": "", + }) + + { + chan1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid)) + tt.AssertEqual(t, "channels.description_name", nil, chan1["description_name"]) + } + + // [8] fail to update description_name + + tt.RequestAuthPatchShouldFail(t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels/%s", uid, chanid), gin.H{ + "description_name": strings.Repeat("0123456789", 48), + }, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG) + +} + //TODO test missing channel-xx methods