ListChannelMessages()

This commit is contained in:
Mike Schwörer 2022-11-20 00:30:30 +01:00
parent 80f3b982d2
commit b56c021356
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
5 changed files with 261 additions and 4 deletions

View File

@ -494,7 +494,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID)
if err == sql.ErrNoRows { 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 { if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) 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())) return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON()))
} }
func (h APIHandler) GetChannelMessages(g *gin.Context) ginresp.HTTPResponse { // ListChannelMessages swaggerdoc
return ginresp.NotImplemented() //TODO //
// @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 // ListUserSubscriptions swaggerdoc

View File

@ -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", ginresp.Wrap(r.apiHandler.ListChannels))
apiv2.GET("/users/:uid/channels/:cid", ginresp.Wrap(r.apiHandler.GetChannel)) 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/channels/:cid/subscriptions", ginresp.Wrap(r.apiHandler.ListChannelSubscriptions))
apiv2.GET("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions)) apiv2.GET("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions))

View File

@ -140,3 +140,38 @@ func (db *Database) ListMessages(ctx TxContext, userid int64, pageSize int, inTo
return data[0:pageSize], outToken, nil 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
}
}

View File

@ -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": { "/api-v2/users/{uid}/channels/{cid}/subscriptions": {
"get": { "get": {
"summary": "List all subscriptions of a channel", "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": { "handler.ListChannelSubscriptions.response": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -102,6 +102,17 @@ definitions:
user_key: user_key:
type: string type: string
type: object 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: handler.ListChannelSubscriptions.response:
properties: properties:
subscriptions: subscriptions:
@ -707,6 +718,49 @@ paths:
schema: schema:
$ref: '#/definitions/ginresp.apiError' $ref: '#/definitions/ginresp.apiError'
summary: List all channels of a user 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: /api-v2/users/{uid}/channels/{cid}/subscriptions:
get: get:
operationId: api-chan-subscriptions-list operationId: api-chan-subscriptions-list