diff --git a/server/api/apierr/enums.go b/server/api/apierr/enums.go index d58891b..cf9af6f 100644 --- a/server/api/apierr/enums.go +++ b/server/api/apierr/enums.go @@ -18,6 +18,7 @@ const ( BINDFAIL_QUERY_PARAM APIError = 1151 BINDFAIL_BODY_PARAM APIError = 1152 BINDFAIL_URI_PARAM APIError = 1153 + INVALID_ENUM_VALUE APIError = 1171 NO_TITLE APIError = 1201 TITLE_TOO_LONG APIError = 1202 diff --git a/server/api/handler/api.go b/server/api/handler/api.go index 1a030fa..71930f5 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -423,28 +423,39 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { // ListChannels swaggerdoc // -// @Summary List all channels of a user -// @ID api-channels-list +// @Summary List channels of a user (subscribed/owned) +// @Description The possible values for 'selector' are: +// @Description - "owned" Return all channels of the user +// @Description - "subscribed" Return all channels that the user is subscribing to +// @Description - "all" Return channels that the user owns or is subscribing +// @Description - "subscribed_any" Return all channels that the user is subscribing to (even unconfirmed) +// @Description - "all_any" Return channels that the user owns or is subscribing (even unconfirmed) +// @ID api-channels-list // -// @Param uid path int true "UserID" +// @Param uid path int true "UserID" +// @Param selector query string true "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any) // -// @Success 200 {object} handler.ListChannels.response -// @Failure 400 {object} ginresp.apiError -// @Failure 401 {object} ginresp.apiError -// @Failure 404 {object} ginresp.apiError -// @Failure 500 {object} ginresp.apiError +// @Success 200 {object} handler.ListChannels.response +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError // -// @Router /api-v2/users/{uid}/channels [GET] +// @Router /api-v2/users/{uid}/channels [GET] func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse { type uri struct { UserID int64 `uri:"uid"` } + type query struct { + Selector *string `form:"selector"` + } type response struct { Channels []models.ChannelJSON `json:"channels"` } var u uri - ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) + var q query + ctx, errResp := h.app.StartRequest(g, &u, &q, nil, nil) if errResp != nil { return *errResp } @@ -454,12 +465,43 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse { return *permResp } - clients, err := h.database.ListChannels(ctx, u.UserID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) - } + sel := strings.ToLower(langext.Coalesce(q.Selector, "owned")) - res := langext.ArrMap(clients, func(v models.Channel) models.ChannelJSON { return v.JSON() }) + var res []models.ChannelJSON + + if sel == "owned" { + channels, err := h.database.ListChannelsByOwner(ctx, u.UserID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(true) }) + } else if sel == "subscribed_any" { + channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, false) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(false) }) + } else if sel == "all_any" { + channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, false) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(false) }) + } else if sel == "subscribed" { + channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, true) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(false) }) + } else if sel == "all" { + channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, true) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(false) }) + } else { + return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil) + } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Channels: res})) } @@ -504,7 +546,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } - return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON())) + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON(true))) } // ListChannelMessages swaggerdoc @@ -1131,11 +1173,11 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { // // @Param post_data query handler.CreateMessage.body false " " // -// @Success 200 {object} models.MessageJSON -// @Failure 400 {object} ginresp.apiError -// @Failure 401 {object} ginresp.apiError -// @Failure 404 {object} ginresp.apiError -// @Failure 500 {object} ginresp.apiError +// @Success 200 {object} models.MessageJSON +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError // // @Router /api-v2/messages [POST] func (h APIHandler) CreateMessage(g *gin.Context) ginresp.HTTPResponse { diff --git a/server/db/channels.go b/server/db/channels.go index 903988f..4d10eee 100644 --- a/server/db/channels.go +++ b/server/db/channels.go @@ -6,28 +6,6 @@ import ( "time" ) -func (db *Database) GetChannelByKey(ctx TxContext, key string) (*models.Channel, error) { - tx, err := ctx.GetOrCreateTransaction(db) - if err != nil { - return nil, err - } - - rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE subscribe_key = ? OR send_key = ? LIMIT 1", key, key) - if err != nil { - return nil, err - } - - channel, err := models.DecodeChannel(rows) - if err == sql.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, err - } - - return &channel, nil -} - func (db *Database) GetChannelByName(ctx TxContext, userid int64, chanName string) (*models.Channel, error) { tx, err := ctx.GetOrCreateTransaction(db) if err != nil { @@ -85,7 +63,7 @@ func (db *Database) CreateChannel(ctx TxContext, userid int64, name string, subs }, nil } -func (db *Database) ListChannels(ctx TxContext, userid int64) ([]models.Channel, error) { +func (db *Database) ListChannelsByOwner(ctx TxContext, userid int64) ([]models.Channel, error) { tx, err := ctx.GetOrCreateTransaction(db) if err != nil { return nil, err @@ -104,6 +82,56 @@ func (db *Database) ListChannels(ctx TxContext, userid int64) ([]models.Channel, return data, nil } +func (db *Database) ListChannelsBySubscriber(ctx TxContext, userid int64, confirmed bool) ([]models.Channel, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return nil, err + } + + confCond := "" + if confirmed { + confCond = " AND sub.confirmed = 1" + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM channels LEFT JOIN subscriptions sub on channels.channel_id = sub.channel_id WHERE sub.subscriber_user_id = ? "+confCond, + userid) + if err != nil { + return nil, err + } + + data, err := models.DecodeChannels(rows) + if err != nil { + return nil, err + } + + return data, nil +} + +func (db *Database) ListChannelsByAccess(ctx TxContext, userid int64, confirmed bool) ([]models.Channel, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return nil, err + } + + confCond := "sub.subscriber_user_id = ?" + if confirmed { + confCond = "(sub.subscriber_user_id = ? AND sub.confirmed = 1)" + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM channels LEFT JOIN subscriptions sub on channels.channel_id = sub.channel_id WHERE owner_user_id = ? OR "+confCond, + userid) + if err != nil { + return nil, err + } + + data, err := models.DecodeChannels(rows) + if err != nil { + return nil, err + } + + return data, nil +} + func (db *Database) GetChannel(ctx TxContext, userid int64, channelid int64) (models.Channel, error) { tx, err := ctx.GetOrCreateTransaction(db) if err != nil { diff --git a/server/models/channel.go b/server/models/channel.go index 13f063e..b19f5e2 100644 --- a/server/models/channel.go +++ b/server/models/channel.go @@ -18,13 +18,13 @@ type Channel struct { MessagesSent int } -func (c Channel) JSON() ChannelJSON { +func (c Channel) JSON(includeKey bool) ChannelJSON { return ChannelJSON{ ChannelID: c.ChannelID, OwnerUserID: c.OwnerUserID, Name: c.Name, - SubscribeKey: c.SubscribeKey, - SendKey: c.SendKey, + SubscribeKey: langext.Conditional(includeKey, langext.Ptr(c.SubscribeKey), nil), + SendKey: langext.Conditional(includeKey, langext.Ptr(c.SendKey), nil), TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano), TimestampLastSent: timeOptFmt(c.TimestampLastSent, time.RFC3339Nano), MessagesSent: c.MessagesSent, @@ -35,8 +35,8 @@ type ChannelJSON struct { ChannelID int64 `json:"channel_id"` OwnerUserID int64 `json:"owner_user_id"` Name string `json:"name"` - SubscribeKey string `json:"subscribe_key"` - SendKey string `json:"send_key"` + 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"` TimestampLastSent *string `json:"timestamp_last_sent"` MessagesSent int `json:"messages_sent"` diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index f0ed843..71347fc 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -66,6 +66,51 @@ "schema": { "$ref": "#/definitions/handler.SendMessage.body" } + }, + { + "type": "string", + "name": "chan_key", + "in": "formData" + }, + { + "type": "string", + "name": "channel", + "in": "formData" + }, + { + "type": "string", + "name": "content", + "in": "formData" + }, + { + "type": "string", + "name": "msg_id", + "in": "formData" + }, + { + "type": "integer", + "name": "priority", + "in": "formData" + }, + { + "type": "number", + "name": "timestamp", + "in": "formData" + }, + { + "type": "string", + "name": "title", + "in": "formData" + }, + { + "type": "integer", + "name": "user_id", + "in": "formData" + }, + { + "type": "string", + "name": "user_key", + "in": "formData" } ], "responses": { @@ -167,6 +212,80 @@ } } } + }, + "post": { + "description": "This is similar to the main route `POST -\u003e https://scn.blackfrestbytes.com/`\nBut this route can change in the future, for long-living scripts etc. it's better to use the normal POST route", + "summary": "Create a new message", + "operationId": "api-messages-create", + "parameters": [ + { + "type": "string", + "name": "chan_key", + "in": "query" + }, + { + "type": "string", + "name": "channel", + "in": "query" + }, + { + "type": "string", + "name": "content", + "in": "query" + }, + { + "type": "string", + "name": "msg_id", + "in": "query" + }, + { + "type": "integer", + "name": "priority", + "in": "query" + }, + { + "type": "number", + "name": "timestamp", + "in": "query" + }, + { + "type": "string", + "name": "title", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MessageJSON" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } } }, "/api-v2/messages/{mid}": { @@ -349,6 +468,7 @@ }, "/api-v2/users/{uid}/channels": { "get": { + "description": "The possible values for 'selector' are:\n- \"owned\" Return all channels of the user\n- \"subscribed\" Return all channels that the user is subscribing to\n- \"all\" Return channels that the user owns or is subscribing\n- \"subscribed_any\" Return all channels that the user is subscribing to (even unconfirmed)\n- \"all_any\" Return channels that the user owns or is subscribing (even unconfirmed)", "summary": "List all channels of a user", "operationId": "api-channels-list", "parameters": [ @@ -358,6 +478,20 @@ "name": "uid", "in": "path", "required": true + }, + { + "enum": [ + "owned", + "subscribed", + "all", + "subscribed_any", + "all_any" + ], + "type": "string", + "description": "Filter channels (default: owned)", + "name": "selector", + "in": "query", + "required": true } ], "responses": { @@ -1569,6 +1703,51 @@ "schema": { "$ref": "#/definitions/handler.SendMessage.body" } + }, + { + "type": "string", + "name": "chan_key", + "in": "formData" + }, + { + "type": "string", + "name": "channel", + "in": "formData" + }, + { + "type": "string", + "name": "content", + "in": "formData" + }, + { + "type": "string", + "name": "msg_id", + "in": "formData" + }, + { + "type": "integer", + "name": "priority", + "in": "formData" + }, + { + "type": "number", + "name": "timestamp", + "in": "formData" + }, + { + "type": "string", + "name": "title", + "in": "formData" + }, + { + "type": "integer", + "name": "user_id", + "in": "formData" + }, + { + "type": "string", + "name": "user_key", + "in": "formData" } ], "responses": { @@ -2049,7 +2228,7 @@ "handler.SendMessage.body": { "type": "object", "properties": { - "chanKey": { + "chan_key": { "type": "string" }, "channel": { @@ -2225,10 +2404,8 @@ "owner_user_id": { "type": "integer" }, - "send_key": { - "type": "string" - }, "subscribe_key": { + "description": "can be nil, depending on endpoint", "type": "string" }, "timestamp_created": { diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index a289464..a090e00 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -209,7 +209,7 @@ definitions: type: object handler.SendMessage.body: properties: - chanKey: + chan_key: type: string channel: type: string @@ -324,9 +324,8 @@ definitions: type: string owner_user_id: type: integer - send_key: - type: string subscribe_key: + description: can be nil, depending on endpoint type: string timestamp_created: type: string @@ -480,6 +479,33 @@ paths: name: post_body schema: $ref: '#/definitions/handler.SendMessage.body' + - in: formData + name: chan_key + type: string + - in: formData + name: channel + type: string + - in: formData + name: content + type: string + - in: formData + name: msg_id + type: string + - in: formData + name: priority + type: integer + - in: formData + name: timestamp + type: number + - in: formData + name: title + type: string + - in: formData + name: user_id + type: integer + - in: formData + name: user_key + type: string responses: "200": description: OK @@ -549,6 +575,55 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' summary: List all (subscribed) messages + post: + description: |- + This is similar to the main route `POST -> https://scn.blackfrestbytes.com/` + But this route can change in the future, for long-living scripts etc. it's better to use the normal POST route + operationId: api-messages-create + parameters: + - in: query + name: chan_key + type: string + - in: query + name: channel + type: string + - in: query + name: content + type: string + - in: query + name: msg_id + type: string + - in: query + name: priority + type: integer + - in: query + name: timestamp + type: number + - in: query + name: title + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.MessageJSON' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ginresp.apiError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/ginresp.apiError' + "404": + description: Not Found + schema: + $ref: '#/definitions/ginresp.apiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + summary: Create a new message /api-v2/messages/{mid}: patch: description: The user must own the message and request the resource with the @@ -669,6 +744,13 @@ paths: summary: (Partially) update a user /api-v2/users/{uid}/channels: get: + description: |- + The possible values for 'selector' are: + - "owned" Return all channels of the user + - "subscribed" Return all channels that the user is subscribing to + - "all" Return channels that the user owns or is subscribing + - "subscribed_any" Return all channels that the user is subscribing to (even unconfirmed) + - "all_any" Return channels that the user owns or is subscribing (even unconfirmed) operationId: api-channels-list parameters: - description: UserID @@ -676,6 +758,17 @@ paths: name: uid required: true type: integer + - description: 'Filter channels (default: owned)' + enum: + - owned + - subscribed + - all + - subscribed_any + - all_any + in: query + name: selector + required: true + type: string responses: "200": description: OK @@ -1489,6 +1582,33 @@ paths: name: post_body schema: $ref: '#/definitions/handler.SendMessage.body' + - in: formData + name: chan_key + type: string + - in: formData + name: channel + type: string + - in: formData + name: content + type: string + - in: formData + name: msg_id + type: string + - in: formData + name: priority + type: integer + - in: formData + name: timestamp + type: number + - in: formData + name: title + type: string + - in: formData + name: user_id + type: integer + - in: formData + name: user_key + type: string responses: "200": description: OK