2023-05-28 13:39:20 +02:00
package handler
import (
2024-07-16 17:19:55 +02:00
"blackforestbytes.com/simplecloudnotifier/logic"
2023-10-14 21:37:00 +02:00
"database/sql"
"errors"
2024-07-15 17:26:55 +02:00
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
2023-10-14 21:37:00 +02:00
"net/http"
"strings"
"time"
2023-05-28 13:39:20 +02:00
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
)
// ListMessages swaggerdoc
//
// @Summary List all (subscribed) messages
// @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-messages-list
// @Tags API-v2
//
// @Param query_data query handler.ListMessages.query false " "
//
// @Success 200 {object} handler.ListMessages.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/messages [GET]
2024-07-15 17:26:55 +02:00
func ( h APIHandler ) ListMessages ( pctx ginext . PreContext ) ginext . HTTPResponse {
2023-05-28 13:39:20 +02:00
type query struct {
2023-05-28 14:05:53 +02:00
PageSize * int ` json:"page_size" form:"page_size" `
NextPageToken * string ` json:"next_page_token" form:"next_page_token" `
2024-09-20 20:37:55 +02:00
Search [ ] string ` json:"search" form:"search" `
StringSearch [ ] string ` json:"string_search" form:"string_search" `
2023-05-28 14:05:53 +02:00
Trimmed * bool ` json:"trimmed" form:"trimmed" `
Channels [ ] string ` json:"channel" form:"channel" `
ChannelIDs [ ] string ` json:"channel_id" form:"channel_id" `
Senders [ ] string ` json:"sender" form:"sender" `
TimeBefore * string ` json:"before" form:"before" ` // RFC3339
TimeAfter * string ` json:"after" form:"after" ` // RFC3339
Priority [ ] int ` json:"priority" form:"priority" `
KeyTokens [ ] string ` json:"used_key" form:"used_key" `
2024-09-20 15:36:16 +02:00
HasSender * bool ` json:"has_sender" form:"has_sender" `
2023-05-28 13:39:20 +02:00
}
type response struct {
2024-09-15 21:07:46 +02:00
Messages [ ] models . Message ` json:"messages" `
NextPageToken string ` json:"next_page_token" `
PageSize int ` json:"page_size" `
2024-09-20 15:36:16 +02:00
TotalCount int64 ` json:"total_count" `
2023-05-28 13:39:20 +02:00
}
var q query
2024-07-16 17:19:55 +02:00
ctx , g , errResp := pctx . Query ( & q ) . Start ( )
2023-05-28 13:39:20 +02:00
if errResp != nil {
return * errResp
}
defer ctx . Cancel ( )
2024-09-16 15:17:20 +02:00
return h . app . DoRequest ( ctx , g , models . TLockReadWrite , func ( ctx * logic . AppContext , finishSuccess func ( r ginext . HTTPResponse ) ginext . HTTPResponse ) ginext . HTTPResponse {
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
trimmed := langext . Coalesce ( q . Trimmed , true )
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
maxPageSize := langext . Conditional ( trimmed , 16 , 256 )
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
pageSize := mathext . Clamp ( langext . Coalesce ( q . PageSize , 64 ) , 1 , maxPageSize )
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
if permResp := ctx . CheckPermissionSelfAllMessagesRead ( ) ; permResp != nil {
return * permResp
}
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
userid := * ctx . GetPermissionUserID ( )
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
tok , err := ct . Decode ( langext . Coalesce ( q . NextPageToken , "" ) )
if err != nil {
return ginresp . APIError ( g , 400 , apierr . PAGETOKEN_ERROR , "Failed to decode next_page_token" , err )
}
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
err = h . database . UpdateUserLastRead ( ctx , userid )
if err != nil {
return ginresp . APIError ( g , 500 , apierr . DATABASE_ERROR , "Failed to update last-read" , err )
}
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
filter := models . MessageFilter {
ConfirmedSubscriptionBy : langext . Ptr ( userid ) ,
}
2023-05-28 13:39:20 +02:00
2024-09-20 20:37:55 +02:00
if len ( q . Search ) != 0 {
filter . SearchStringFTS = langext . Ptr ( langext . ArrMap ( q . Search , func ( v string ) string { return strings . TrimSpace ( v ) } ) )
}
if len ( q . StringSearch ) != 0 {
filter . SearchStringPlain = langext . Ptr ( langext . ArrMap ( q . StringSearch , func ( v string ) string { return strings . TrimSpace ( v ) } ) )
2024-07-16 17:19:55 +02:00
}
2023-05-28 14:05:53 +02:00
2024-07-16 17:19:55 +02:00
if len ( q . Channels ) != 0 {
filter . ChannelNameCS = langext . Ptr ( q . Channels )
}
if len ( q . ChannelIDs ) != 0 {
cids := make ( [ ] models . ChannelID , 0 , len ( q . ChannelIDs ) )
for _ , v := range q . ChannelIDs {
cid := models . ChannelID ( v )
if err = cid . Valid ( ) ; err != nil {
return ginresp . APIError ( g , 400 , apierr . BINDFAIL_QUERY_PARAM , "Invalid channel-id" , err )
}
cids = append ( cids , cid )
2023-05-28 14:05:53 +02:00
}
2024-07-16 17:19:55 +02:00
filter . ChannelID = & cids
2023-05-28 14:05:53 +02:00
}
2024-07-16 17:19:55 +02:00
if len ( q . Senders ) != 0 {
filter . SenderNameCS = langext . Ptr ( q . Senders )
}
2023-05-28 14:05:53 +02:00
2024-09-20 15:36:16 +02:00
if q . HasSender != nil {
filter . HasSenderName = langext . Ptr ( * q . HasSender )
}
2024-07-16 17:19:55 +02:00
if q . TimeBefore != nil {
t0 , err := time . Parse ( time . RFC3339 , * q . TimeBefore )
if err != nil {
return ginresp . APIError ( g , 400 , apierr . BINDFAIL_QUERY_PARAM , "Invalid before-time" , err )
}
filter . TimestampCoalesceBefore = & t0
2023-05-28 14:05:53 +02:00
}
2024-07-16 17:19:55 +02:00
if q . TimeAfter != nil {
t0 , err := time . Parse ( time . RFC3339 , * q . TimeAfter )
if err != nil {
return ginresp . APIError ( g , 400 , apierr . BINDFAIL_QUERY_PARAM , "Invalid after-time" , err )
}
filter . TimestampCoalesceAfter = & t0
2023-05-28 14:05:53 +02:00
}
2024-07-16 17:19:55 +02:00
if len ( q . Priority ) != 0 {
filter . Priority = langext . Ptr ( q . Priority )
}
2023-05-28 14:05:53 +02:00
2024-07-16 17:19:55 +02:00
if len ( q . KeyTokens ) != 0 {
tids := make ( [ ] models . KeyTokenID , 0 , len ( q . KeyTokens ) )
for _ , v := range q . KeyTokens {
tid := models . KeyTokenID ( v )
if err = tid . Valid ( ) ; err != nil {
return ginresp . APIError ( g , 400 , apierr . BINDFAIL_QUERY_PARAM , "Invalid keytoken-id" , err )
}
tids = append ( tids , tid )
2023-05-28 14:05:53 +02:00
}
2024-07-16 17:19:55 +02:00
filter . UsedKeyID = & tids
2023-05-28 14:05:53 +02:00
}
2024-09-20 15:36:16 +02:00
messages , npt , totalCount , err := h . database . ListMessages ( ctx , filter , & pageSize , tok )
2024-07-16 17:19:55 +02:00
if err != nil {
return ginresp . APIError ( g , 500 , apierr . DATABASE_ERROR , "Failed to query messages" , err )
}
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
if trimmed {
2024-09-15 21:07:46 +02:00
res := langext . ArrMap ( messages , func ( v models . Message ) models . Message { return v . PreMarshal ( ) . Trim ( ) } )
2024-09-20 15:36:16 +02:00
return finishSuccess ( ginext . JSON ( http . StatusOK , response { Messages : res , NextPageToken : npt . Token ( ) , PageSize : pageSize , TotalCount : totalCount } ) )
2024-07-16 17:19:55 +02:00
} else {
2024-09-15 21:07:46 +02:00
res := langext . ArrMap ( messages , func ( v models . Message ) models . Message { return v . PreMarshal ( ) } )
2024-09-20 15:36:16 +02:00
return finishSuccess ( ginext . JSON ( http . StatusOK , response { Messages : res , NextPageToken : npt . Token ( ) , PageSize : pageSize , TotalCount : totalCount } ) )
2024-07-16 17:19:55 +02:00
}
} )
2023-05-28 13:39:20 +02:00
}
// GetMessage swaggerdoc
//
// @Summary Get a single message (untrimmed)
// @Description The user must either own the message and request the resource with the READ or ADMIN Key
// @Description Or the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key
// @Description The returned message is never trimmed
// @ID api-messages-get
// @Tags API-v2
//
2023-05-28 17:04:44 +02:00
// @Param mid path string true "MessageID"
2023-05-28 13:39:20 +02:00
//
2024-09-15 21:07:46 +02:00
// @Success 200 {object} models.Message
2023-05-28 13:39:20 +02:00
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
2023-10-14 21:37:00 +02:00
// @Router /api/v2/messages/{mid} [GET]
2024-07-15 17:26:55 +02:00
func ( h APIHandler ) GetMessage ( pctx ginext . PreContext ) ginext . HTTPResponse {
2023-05-28 13:39:20 +02:00
type uri struct {
MessageID models . MessageID ` uri:"mid" binding:"entityid" `
}
var u uri
2024-07-16 17:19:55 +02:00
ctx , g , errResp := pctx . URI ( & u ) . Start ( )
2023-05-28 13:39:20 +02:00
if errResp != nil {
return * errResp
}
defer ctx . Cancel ( )
2024-09-16 15:17:20 +02:00
return h . app . DoRequest ( ctx , g , models . TLockRead , func ( ctx * logic . AppContext , finishSuccess func ( r ginext . HTTPResponse ) ginext . HTTPResponse ) ginext . HTTPResponse {
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
if permResp := ctx . CheckPermissionAny ( ) ; permResp != nil {
return * permResp
}
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
msg , err := h . database . GetMessage ( ctx , u . MessageID , false )
if errors . Is ( err , sql . ErrNoRows ) {
return ginresp . APIError ( g , 404 , apierr . MESSAGE_NOT_FOUND , "message not found" , err )
}
2023-05-28 13:39:20 +02:00
if err != nil {
2024-07-16 17:19:55 +02:00
return ginresp . APIError ( g , 500 , apierr . DATABASE_ERROR , "Failed to query message" , err )
2023-05-28 13:39:20 +02:00
}
2024-07-16 17:19:55 +02:00
// either we have direct read permissions (it is our message + read/admin key)
// or we subscribe (+confirmed) to the channel and have read/admin key
if ctx . CheckPermissionMessageRead ( msg ) {
2024-09-15 21:07:46 +02:00
return finishSuccess ( ginext . JSON ( http . StatusOK , msg . PreMarshal ( ) ) )
2023-05-28 13:39:20 +02:00
}
2024-07-16 17:19:55 +02:00
if uid := ctx . GetPermissionUserID ( ) ; uid != nil && ctx . CheckPermissionUserRead ( * uid ) == nil {
sub , err := h . database . GetSubscriptionBySubscriber ( ctx , * uid , msg . ChannelID )
if err != nil {
return ginresp . APIError ( g , 500 , apierr . DATABASE_ERROR , "Failed to query subscription" , err )
}
if sub == nil {
// not subbed
return ginresp . APIError ( g , 401 , apierr . USER_AUTH_FAILED , "You are not authorized for this action" , nil )
}
if ! sub . Confirmed {
// sub not confirmed
return ginresp . APIError ( g , 401 , apierr . USER_AUTH_FAILED , "You are not authorized for this action" , nil )
}
// => perm okay
2024-09-15 21:07:46 +02:00
return finishSuccess ( ginext . JSON ( http . StatusOK , msg . PreMarshal ( ) ) )
2023-05-28 13:39:20 +02:00
}
2024-07-16 17:19:55 +02:00
return ginresp . APIError ( g , 401 , apierr . USER_AUTH_FAILED , "You are not authorized for this action" , nil )
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
} )
2023-05-28 13:39:20 +02:00
}
// DeleteMessage swaggerdoc
//
// @Summary Delete a single message
// @Description The user must own the message and request the resource with the ADMIN Key
// @ID api-messages-delete
// @Tags API-v2
//
2023-05-28 17:04:44 +02:00
// @Param mid path string true "MessageID"
2023-05-28 13:39:20 +02:00
//
2024-09-15 21:07:46 +02:00
// @Success 200 {object} models.Message
2023-05-28 13:39:20 +02:00
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/messages/{mid} [DELETE]
2024-07-15 17:26:55 +02:00
func ( h APIHandler ) DeleteMessage ( pctx ginext . PreContext ) ginext . HTTPResponse {
2023-05-28 13:39:20 +02:00
type uri struct {
MessageID models . MessageID ` uri:"mid" binding:"entityid" `
}
var u uri
2024-07-16 17:19:55 +02:00
ctx , g , errResp := pctx . URI ( & u ) . Start ( )
2023-05-28 13:39:20 +02:00
if errResp != nil {
return * errResp
}
defer ctx . Cancel ( )
2024-09-16 15:17:20 +02:00
return h . app . DoRequest ( ctx , g , models . TLockReadWrite , func ( ctx * logic . AppContext , finishSuccess func ( r ginext . HTTPResponse ) ginext . HTTPResponse ) ginext . HTTPResponse {
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
if permResp := ctx . CheckPermissionAny ( ) ; permResp != nil {
return * permResp
}
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
msg , err := h . database . GetMessage ( ctx , u . MessageID , false )
if errors . Is ( err , sql . ErrNoRows ) {
return ginresp . APIError ( g , 404 , apierr . MESSAGE_NOT_FOUND , "message not found" , err )
}
if err != nil {
return ginresp . APIError ( g , 500 , apierr . DATABASE_ERROR , "Failed to query message" , err )
}
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
if ! ctx . CheckPermissionMessageDelete ( msg ) {
return ginresp . APIError ( g , 401 , apierr . USER_AUTH_FAILED , "You are not authorized for this action" , nil )
}
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
err = h . database . DeleteMessage ( ctx , msg . MessageID )
if err != nil {
return ginresp . APIError ( g , 500 , apierr . DATABASE_ERROR , "Failed to delete message" , err )
}
err = h . database . CancelPendingDeliveries ( ctx , msg . MessageID )
if err != nil {
return ginresp . APIError ( g , 500 , apierr . DATABASE_ERROR , "Failed to cancel deliveries" , err )
}
2024-09-15 21:07:46 +02:00
return finishSuccess ( ginext . JSON ( http . StatusOK , msg . PreMarshal ( ) ) )
2023-05-28 13:39:20 +02:00
2024-07-16 17:19:55 +02:00
} )
2023-05-28 13:39:20 +02:00
}