diff --git a/server/api/handler/api.go b/server/api/handler/api.go index 46f8604..f200acf 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -494,7 +494,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Channel not found", err) + return ginresp.InternAPIError(404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) @@ -503,8 +503,98 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON())) } -func (h APIHandler) GetChannelMessages(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() //TODO +// ListChannelMessages swaggerdoc +// +// @Summary List messages of a channel +// @Description The next_page_token is an opaque token, the special value "@start" (or empty-string) is the beginning and "@end" is the end +// @Description Simply start the pagination without a next_page_token and get the next page by calling this endpoint with the returned next_page_token of the last query +// @Description If there are no more entries the token "@end" will be returned +// @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size) +// @ID api-channel-messages +// +// @Param query_data query handler.ListChannelMessages.query false " " +// +// @Success 200 {object} handler.ListChannelMessages.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/{cid}/messages [GET] +func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse { + type uri struct { + ChannelUserID int64 `uri:"uid"` + ChannelID int64 `uri:"cid"` + } + type query struct { + PageSize *int `form:"page_size"` + NextPageToken *string `form:"next_page_token"` + Filter *string `form:"filter"` + Trimmed *bool `form:"trimmed"` + } + type response struct { + Messages []models.MessageJSON `json:"messages"` + NextPageToken string `json:"next_page_token"` + PageSize int `json:"page_size"` + } + + var u uri + var q query + ctx, errResp := h.app.StartRequest(g, &u, &q, nil) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + trimmed := langext.Coalesce(q.Trimmed, true) + + maxPageSize := langext.Conditional(trimmed, 16, 256) + + pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) + + if permResp := ctx.CheckPermissionRead(); permResp != nil { + return *permResp + } + + channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID) + if err == sql.ErrNoRows { + return ginresp.InternAPIError(404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + } + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } + + userid := *ctx.GetPermissionUserID() + + sub, err := h.database.GetSubscriptionBySubscriber(ctx, userid, channel.ChannelID) + if err == sql.ErrNoRows { + return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + } + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + } + if !sub.Confirmed { + return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + } + + tok, err := cursortoken.Decode(langext.Coalesce(q.NextPageToken, "")) + if err != nil { + return ginresp.InternAPIError(500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) + } + + messages, npt, err := h.database.ListChannelMessages(ctx, channel.ChannelID, pageSize, tok) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query messages", err) + } + + var res []models.MessageJSON + if trimmed { + res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.TrimmedJSON() }) + } else { + res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.FullJSON() }) + } + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize})) } // ListUserSubscriptions swaggerdoc diff --git a/server/api/router.go b/server/api/router.go index ca099d1..7376c3a 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -105,7 +105,7 @@ func (r *Router) Init(e *gin.Engine) { apiv2.GET("/users/:uid/channels", ginresp.Wrap(r.apiHandler.ListChannels)) apiv2.GET("/users/:uid/channels/:cid", ginresp.Wrap(r.apiHandler.GetChannel)) - apiv2.GET("/users/:uid/channels/:cid/messages", ginresp.Wrap(r.apiHandler.GetChannelMessages)) + apiv2.GET("/users/:uid/channels/:cid/messages", ginresp.Wrap(r.apiHandler.ListChannelMessages)) apiv2.GET("/users/:uid/channels/:cid/subscriptions", ginresp.Wrap(r.apiHandler.ListChannelSubscriptions)) apiv2.GET("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions)) diff --git a/server/db/messages.go b/server/db/messages.go index 8cf051f..7d1980c 100644 --- a/server/db/messages.go +++ b/server/db/messages.go @@ -140,3 +140,38 @@ func (db *Database) ListMessages(ctx TxContext, userid int64, pageSize int, inTo return data[0:pageSize], outToken, nil } } + +func (db *Database) ListChannelMessages(ctx TxContext, channelid int64, pageSize int, inTok cursortoken.CursorToken) ([]models.Message, cursortoken.CursorToken, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return nil, cursortoken.CursorToken{}, err + } + + if inTok.Mode == cursortoken.CTMEnd { + return make([]models.Message, 0), cursortoken.End(), nil + } + + pageCond := "" + if inTok.Mode == cursortoken.CTMNormal { + pageCond = fmt.Sprintf("AND ( timestamp_real < %d OR (timestamp_real = %d AND scn_message_id < %d ) )", inTok.Timestamp, inTok.Timestamp, inTok.Id) + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM messages WHERE channel_id = ? "+pageCond+" ORDER BY timestamp_real DESC LIMIT ?", + channelid, + pageSize+1) + if err != nil { + return nil, cursortoken.CursorToken{}, err + } + + data, err := models.DecodeMessages(rows) + if err != nil { + return nil, cursortoken.CursorToken{}, err + } + + if len(data) <= pageSize { + return data, cursortoken.End(), nil + } else { + outToken := cursortoken.Normal(data[pageSize-1].TimestampReal, data[pageSize-1].SCNMessageID, "DESC") + return data[0:pageSize], outToken, nil + } +} diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index db559ef..9ec41e2 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -448,6 +448,67 @@ } } }, + "/api-v2/users/{uid}/channels/{cid}/messages": { + "get": { + "description": "The next_page_token is an opaque token, the special value \"@start\" (or empty-string) is the beginning and \"@end\" is the end\nSimply start the pagination without a next_page_token and get the next page by calling this endpoint with the returned next_page_token of the last query\nIf there are no more entries the token \"@end\" will be returned\nBy default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size)", + "summary": "List messages of a channel", + "operationId": "api-channel-messages", + "parameters": [ + { + "type": "string", + "name": "filter", + "in": "query" + }, + { + "type": "string", + "name": "nextPageToken", + "in": "query" + }, + { + "type": "integer", + "name": "pageSize", + "in": "query" + }, + { + "type": "boolean", + "name": "trimmed", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ListChannelMessages.response" + } + }, + "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/users/{uid}/channels/{cid}/subscriptions": { "get": { "summary": "List all subscriptions of a channel", @@ -1573,6 +1634,23 @@ } } }, + "handler.ListChannelMessages.response": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MessageJSON" + } + }, + "next_page_token": { + "type": "string" + }, + "page_size": { + "type": "integer" + } + } + }, "handler.ListChannelSubscriptions.response": { "type": "object", "properties": { diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index 4319586..5cd3454 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -102,6 +102,17 @@ definitions: user_key: type: string type: object + handler.ListChannelMessages.response: + properties: + messages: + items: + $ref: '#/definitions/models.MessageJSON' + type: array + next_page_token: + type: string + page_size: + type: integer + type: object handler.ListChannelSubscriptions.response: properties: subscriptions: @@ -707,6 +718,49 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' summary: List all channels of a user + /api-v2/users/{uid}/channels/{cid}/messages: + get: + description: |- + The next_page_token is an opaque token, the special value "@start" (or empty-string) is the beginning and "@end" is the end + Simply start the pagination without a next_page_token and get the next page by calling this endpoint with the returned next_page_token of the last query + If there are no more entries the token "@end" will be returned + By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size) + operationId: api-channel-messages + parameters: + - in: query + name: filter + type: string + - in: query + name: nextPageToken + type: string + - in: query + name: pageSize + type: integer + - in: query + name: trimmed + type: boolean + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ListChannelMessages.response' + "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: List messages of a channel /api-v2/users/{uid}/channels/{cid}/subscriptions: get: operationId: api-chan-subscriptions-list