re-implement ack behaviour from version 1.0 for compat

This commit is contained in:
Mike Schwörer 2023-02-03 22:51:03 +01:00
parent 01934e29b1
commit 16f6ab4861
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
16 changed files with 643 additions and 375 deletions

View File

@ -82,3 +82,6 @@
- cannot open sqlite in dbbrowsr (cannot parse schema?)
-> https://github.com/sqlitebrowser/sqlitebrowser/issues/292 -> https://github.com/sqlitebrowser/sqlitebrowser/issues/29266
#### FUTURE
- Remove compat, especially do not create compat id for every new message...

View File

@ -3,7 +3,7 @@ package handler
import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/db/cursortoken"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
@ -930,7 +930,7 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
tok, err := cursortoken.Decode(langext.Coalesce(q.NextPageToken, ""))
tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, ""))
if err != nil {
return ginresp.APIError(g, 500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err)
}
@ -1419,7 +1419,7 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
userid := *ctx.GetPermissionUserID()
tok, err := cursortoken.Decode(langext.Coalesce(q.NextPageToken, ""))
tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, ""))
if err != nil {
return ginresp.APIError(g, 500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err)
}

View File

@ -4,6 +4,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
@ -90,19 +91,6 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
return *errResp
} else {
if okResp.MessageIsOld {
compatMessageID, _, err := h.database.ConvertToCompatID(ctx, okResp.Message.MessageID.String())
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query compat-id", err)
}
if compatMessageID == nil {
v, err := h.database.CreateCompatID(ctx, "messageid", okResp.Message.MessageID.String())
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err)
}
compatMessageID = &v
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{
Success: true,
ErrorID: apierr.NO_ERROR,
@ -113,15 +101,9 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
Quota: okResp.User.QuotaUsedToday(),
IsPro: okResp.User.IsPro,
QuotaMax: okResp.User.QuotaPerDay(),
SCNMessageID: *compatMessageID,
SCNMessageID: okResp.CompatMessageID,
}))
} else {
compatMessageID, err := h.database.CreateCompatID(ctx, "messageid", okResp.Message.MessageID.String())
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{
Success: true,
ErrorID: apierr.NO_ERROR,
@ -132,7 +114,7 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
Quota: okResp.User.QuotaUsedToday() + 1,
IsPro: okResp.User.IsPro,
QuotaMax: okResp.User.QuotaPerDay(),
SCNMessageID: compatMessageID,
SCNMessageID: okResp.CompatMessageID,
}))
}
}
@ -420,12 +402,30 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(204, "Authentification failed")
}
// we no longer ack messages - this is a no-op
messageIdComp, err := h.database.ConvertCompatID(ctx, *data.MessageID, "messageid")
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query messageid<old>", err)
}
if useridCompNew == nil {
return ginresp.SendAPIError(g, 400, apierr.MESSAGE_NOT_FOUND, hl.USER_ID, "Message not found (compat)", nil)
}
ackBefore, err := h.database.GetAck(ctx, models.MessageID(*messageIdComp))
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query ack", err)
}
if !ackBefore {
err = h.database.SetAck(ctx, user.UserID, models.MessageID(*messageIdComp))
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to set ack", err)
}
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{
Success: true,
Message: "ok",
PrevAckValue: 0,
PrevAckValue: langext.Conditional(ackBefore, 1, 0),
NewAckValue: 1,
}))
}
@ -497,11 +497,40 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(204, "Authentification failed")
}
filter := models.MessageFilter{
Owner: langext.Ptr([]models.UserID{user.UserID}),
CompatAcknowledged: langext.Ptr(false),
}
msgs, _, err := h.database.ListMessages(ctx, filter, 16, ct.Start())
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query user")
}
compMsgs := make([]models.CompatMessage, 0, len(msgs))
for _, v := range msgs {
messageIdComp, err := h.database.ConvertToCompatIDOrCreate(ctx, v.MessageID.String(), "messageid")
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create messageid<old>", err)
}
compMsgs = append(compMsgs, models.CompatMessage{
Title: v.Title,
Body: v.Content,
Priority: v.Priority,
Timestamp: v.Timestamp().Unix(),
UserMessageID: v.UserMessageID,
SCNMessageID: messageIdComp,
Trimmed: nil,
})
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{
Success: true,
Message: "ok",
Count: 0,
Data: make([]models.CompatMessage, 0),
Count: len(compMsgs),
Data: compMsgs,
}))
}

View File

@ -20,9 +20,10 @@ import (
)
type SendMessageResponse struct {
User models.User
Message models.Message
MessageIsOld bool
User models.User
Message models.Message
MessageIsOld bool
CompatMessageID int64
}
type MessageHandler struct {
@ -195,11 +196,26 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query existing message", err))
}
if msg != nil {
existingCompID, _, err := h.database.ConvertToCompatID(ctx, msg.MessageID.String())
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query compat-id", err))
}
if existingCompID == nil {
v, err := h.database.CreateCompatID(ctx, "messageid", msg.MessageID.String())
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
}
existingCompID = &v
}
//the found message can be deleted (!), but we still return NO_ERROR here...
return &SendMessageResponse{
User: user,
Message: *msg,
MessageIsOld: true,
User: user,
Message: *msg,
MessageIsOld: true,
CompatMessageID: *existingCompID,
}, nil
}
}
@ -251,6 +267,11 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create message in db", err))
}
cid, err := h.database.CreateCompatID(ctx, "messageid", msg.MessageID.String())
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
}
subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query subscriptions", err))
@ -295,8 +316,9 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
}
return &SendMessageResponse{
User: user,
Message: msg,
MessageIsOld: false,
User: user,
Message: msg,
MessageIsOld: false,
CompatMessageID: cid,
}, nil
}

View File

@ -455,6 +455,14 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
}
}
_, err = dbnew.Exec(ctx, "INSERT INTO compat_acks (user_id, message_id) VALUES (:uid, :mid)", sq.PP{
"uid": userid,
"mid": messageid,
})
if err != nil {
panic(err)
}
} else if len(oldmessage.Ack) == 1 && oldmessage.Ack[0] == 0 {
if clientid != nil {

View File

@ -1,6 +1,7 @@
package primary
import (
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
@ -98,3 +99,59 @@ func (db *Database) ConvertToCompatID(ctx TxContext, newid string) (*int64, *str
return &oldid, &idtype, nil
}
func (db *Database) ConvertToCompatIDOrCreate(ctx TxContext, idtype string, newid string) (int64, error) {
id1, _, err := db.ConvertToCompatID(ctx, newid)
if err != nil {
return 0, err
}
if id1 != nil {
return *id1, nil
}
id2, err := db.CreateCompatID(ctx, idtype, newid)
if err != nil {
return 0, err
}
return id2, nil
}
func (db *Database) GetAck(ctx TxContext, msgid models.MessageID) (bool, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return false, err
}
rows, err := tx.Query(ctx, "SELECT * FROM compat_acks WHERE message_id = :msgid LIMIT 1", sq.PP{
"msgid": msgid,
})
if err != nil {
return false, err
}
res := rows.Next()
err = rows.Close()
if err != nil {
return false, err
}
return res, nil
}
func (db *Database) SetAck(ctx TxContext, userid models.UserID, msgid models.MessageID) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO compat_acks (user_id, message_id) VALUES (:uid, :mid)", sq.PP{
"uid": userid,
"mid": msgid,
})
if err != nil {
return err
}
return nil
}

View File

@ -1,7 +1,7 @@
package primary
import (
"blackforestbytes.com/simplecloudnotifier/db/cursortoken"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
@ -116,18 +116,18 @@ func (db *Database) DeleteMessage(ctx TxContext, messageID models.MessageID) err
return nil
}
func (db *Database) ListMessages(ctx TxContext, filter models.MessageFilter, pageSize int, inTok cursortoken.CursorToken) ([]models.Message, cursortoken.CursorToken, error) {
func (db *Database) ListMessages(ctx TxContext, filter models.MessageFilter, pageSize int, inTok ct.CursorToken) ([]models.Message, ct.CursorToken, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, cursortoken.CursorToken{}, err
return nil, ct.CursorToken{}, err
}
if inTok.Mode == cursortoken.CTMEnd {
return make([]models.Message, 0), cursortoken.End(), nil
if inTok.Mode == ct.CTMEnd {
return make([]models.Message, 0), ct.End(), nil
}
pageCond := "1=1"
if inTok.Mode == cursortoken.CTMNormal {
if inTok.Mode == ct.CTMNormal {
pageCond = "timestamp_real < :tokts OR (timestamp_real = :tokts AND message_id < :tokid )"
}
@ -143,18 +143,18 @@ func (db *Database) ListMessages(ctx TxContext, filter models.MessageFilter, pag
rows, err := tx.Query(ctx, sqlQuery, prepParams)
if err != nil {
return nil, cursortoken.CursorToken{}, err
return nil, ct.CursorToken{}, err
}
data, err := models.DecodeMessages(rows)
if err != nil {
return nil, cursortoken.CursorToken{}, err
return nil, ct.CursorToken{}, err
}
if len(data) <= pageSize {
return data, cursortoken.End(), nil
return data, ct.End(), nil
} else {
outToken := cursortoken.Normal(data[pageSize-1].Timestamp(), data[pageSize-1].MessageID.String(), "DESC", filter.Hash())
outToken := ct.Normal(data[pageSize-1].Timestamp(), data[pageSize-1].MessageID.String(), "DESC", filter.Hash())
return data[0:pageSize], outToken, nil
}
}

View File

@ -183,6 +183,16 @@ CREATE UNIQUE INDEX "idx_compatids_new" ON compat_ids (new);
CREATE UNIQUE INDEX "idx_compatids_old" ON compat_ids (old, type);
CREATE TABLE compat_acks
(
user_id TEXT NOT NULL,
message_id TEXT NOT NULL
) STRICT;
CREATE INDEX "idx_compatacks_userid" ON compat_acks (user_id);
CREATE UNIQUE INDEX "idx_compatacks_messageid" ON compat_acks (message_id);
CREATE UNIQUE INDEX "idx_compatacks_userid_messageid" ON compat_acks (user_id, message_id);
CREATE TABLE `meta`
(
meta_key TEXT NOT NULL,

View File

@ -14,8 +14,8 @@ const (
type Message struct {
MessageID MessageID
SenderUserID UserID
OwnerUserID UserID
SenderUserID UserID // user that sent the message
OwnerUserID UserID // oner of the message (= owner of the channel that contains it)
ChannelInternalName string
ChannelID ChannelID
SenderName *string

View File

@ -39,6 +39,7 @@ type MessageFilter struct {
UserMessageID *[]string
OnlyDeleted bool
IncludeDeleted bool
CompatAcknowledged *bool
}
func (f MessageFilter) SQL() (string, string, sq.PP, error) {
@ -79,7 +80,7 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
if f.Owner != nil {
filter := make([]string, 0)
for i, v := range *f.Sender {
for i, v := range *f.Owner {
filter = append(filter, fmt.Sprintf("(owner_user_id = :owner_%d)", i))
params[fmt.Sprintf("owner_%d", i)] = v
}
@ -209,6 +210,16 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
sqlClauses = append(sqlClauses, "(usr_message_id IS NOT NULL AND ("+strings.Join(filter, " OR ")+"))")
}
if f.CompatAcknowledged != nil {
joinClause += " LEFT JOIN compat_acks AS filter_compatack_compat_acks on messages.message_id = filter_compatack_compat_acks.message_id "
if *f.CompatAcknowledged {
sqlClauses = append(sqlClauses, "(filter_compatack_compat_acks.message_id IS NOT NULL)")
} else {
sqlClauses = append(sqlClauses, "(filter_compatack_compat_acks.message_id IS NULL)")
}
}
if f.SearchString != nil {
filter := make([]string, 0)
for i, v := range *f.SearchString {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -426,165 +426,6 @@
}
}
},
"/api/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)",
"tags": [
"API-v2"
],
"summary": "List all (subscribed) messages",
"operationId": "api-messages-list",
"parameters": [
{
"type": "string",
"name": "filter",
"in": "query"
},
{
"type": "string",
"name": "next_page_token",
"in": "query"
},
{
"type": "integer",
"name": "page_size",
"in": "query"
},
{
"type": "boolean",
"description": "TODO more filter (sender-name, channel, timestamps, prio, )",
"name": "trimmed",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.ListMessages.response"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/messages/{mid}": {
"delete": {
"description": "The user must own the message and request the resource with the ADMIN Key",
"tags": [
"API-v2"
],
"summary": "Delete a single message",
"operationId": "api-messages-delete",
"parameters": [
{
"type": "integer",
"description": "MessageID",
"name": "mid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.MessageJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
},
"patch": {
"description": "The user must either own the message and request the resource with the READ or ADMIN Key\nOr the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key\nThe returned message is never trimmed",
"tags": [
"API-v2"
],
"summary": "Get a single message (untrimmed)",
"operationId": "api-messages-get",
"parameters": [
{
"type": "integer",
"description": "MessageID",
"name": "mid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.MessageJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/ping": {
"get": {
"tags": [
@ -1013,7 +854,166 @@
}
}
},
"/api/users": {
"/api/v2/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)",
"tags": [
"API-v2"
],
"summary": "List all (subscribed) messages",
"operationId": "api-messages-list",
"parameters": [
{
"type": "string",
"name": "filter",
"in": "query"
},
{
"type": "string",
"name": "next_page_token",
"in": "query"
},
{
"type": "integer",
"name": "page_size",
"in": "query"
},
{
"type": "boolean",
"description": "TODO more filter (sender-name, channel, timestamps, prio, )",
"name": "trimmed",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.ListMessages.response"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/messages/{mid}": {
"delete": {
"description": "The user must own the message and request the resource with the ADMIN Key",
"tags": [
"API-v2"
],
"summary": "Delete a single message",
"operationId": "api-messages-delete",
"parameters": [
{
"type": "integer",
"description": "MessageID",
"name": "mid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.MessageJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
},
"patch": {
"description": "The user must either own the message and request the resource with the READ or ADMIN Key\nOr the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key\nThe returned message is never trimmed",
"tags": [
"API-v2"
],
"summary": "Get a single message (untrimmed)",
"operationId": "api-messages-get",
"parameters": [
{
"type": "integer",
"description": "MessageID",
"name": "mid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.MessageJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/users": {
"post": {
"tags": [
"API-v2"
@ -1052,7 +1052,7 @@
}
}
},
"/api/users/{uid}": {
"/api/v2/users/{uid}": {
"get": {
"tags": [
"API-v2"
@ -1191,7 +1191,7 @@
}
}
},
"/api/users/{uid}/channels": {
"/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)",
"tags": [
@ -1305,7 +1305,7 @@
}
}
},
"/api/users/{uid}/channels/{cid}": {
"/api/v2/users/{uid}/channels/{cid}": {
"get": {
"tags": [
"API-v2"
@ -1441,7 +1441,7 @@
}
}
},
"/api/users/{uid}/channels/{cid}/messages": {
"/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)",
"tags": [
@ -1519,7 +1519,7 @@
}
}
},
"/api/users/{uid}/channels/{cid}/subscriptions": {
"/api/v2/users/{uid}/channels/{cid}/subscriptions": {
"get": {
"tags": [
"API-v2"
@ -1576,7 +1576,7 @@
}
}
},
"/api/users/{uid}/clients": {
"/api/v2/users/{uid}/clients": {
"get": {
"tags": [
"API-v2"
@ -1670,7 +1670,7 @@
}
}
},
"/api/users/{uid}/clients/{cid}": {
"/api/v2/users/{uid}/clients/{cid}": {
"get": {
"tags": [
"API-v2"
@ -1782,7 +1782,7 @@
}
}
},
"/api/users/{uid}/subscriptions": {
"/api/v2/users/{uid}/subscriptions": {
"get": {
"description": "The possible values for 'selector' are:\n- \"outgoing_all\" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)\n- \"outgoing_confirmed\" Confirmed subscriptions with the user as subscriber\n- \"outgoing_unconfirmed\" Unconfirmed (Pending) subscriptions with the user as subscriber\n- \"incoming_all\" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)\n- \"incoming_confirmed\" Confirmed subscriptions from other users to channels of this user\n- \"incoming_unconfirmed\" Unconfirmed subscriptions from other users to channels of this user (= requests)",
"tags": [
@ -1898,7 +1898,7 @@
}
}
},
"/api/users/{uid}/subscriptions/{sid}": {
"/api/v2/users/{uid}/subscriptions/{sid}": {
"get": {
"tags": [
"API-v2"
@ -2645,7 +2645,7 @@
"type": "object",
"properties": {
"is_pro": {
"type": "integer"
"type": "boolean"
},
"message": {
"type": "string"

View File

@ -179,7 +179,7 @@ definitions:
handler.Register.response:
properties:
is_pro:
type: integer
type: boolean
message:
type: string
quota:
@ -823,119 +823,6 @@ paths:
summary: Get information about the current user
tags:
- API-v1
/api/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-messages-list
parameters:
- in: query
name: filter
type: string
- in: query
name: next_page_token
type: string
- in: query
name: page_size
type: integer
- description: TODO more filter (sender-name, channel, timestamps, prio, )
in: query
name: trimmed
type: boolean
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.ListMessages.response'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: List all (subscribed) messages
tags:
- API-v2
/api/messages/{mid}:
delete:
description: The user must own the message and request the resource with the
ADMIN Key
operationId: api-messages-delete
parameters:
- description: MessageID
in: path
name: mid
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.MessageJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Delete a single message
tags:
- API-v2
patch:
description: |-
The user must either own the message and request the resource with the READ or ADMIN Key
Or the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key
The returned message is never trimmed
operationId: api-messages-get
parameters:
- description: MessageID
in: path
name: mid
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.MessageJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Get a single message (untrimmed)
tags:
- API-v2
/api/ping:
delete:
responses:
@ -1227,7 +1114,120 @@ paths:
summary: Upgrade a free account to a paid account
tags:
- API-v1
/api/users:
/api/v2/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-messages-list
parameters:
- in: query
name: filter
type: string
- in: query
name: next_page_token
type: string
- in: query
name: page_size
type: integer
- description: TODO more filter (sender-name, channel, timestamps, prio, )
in: query
name: trimmed
type: boolean
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.ListMessages.response'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: List all (subscribed) messages
tags:
- API-v2
/api/v2/messages/{mid}:
delete:
description: The user must own the message and request the resource with the
ADMIN Key
operationId: api-messages-delete
parameters:
- description: MessageID
in: path
name: mid
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.MessageJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Delete a single message
tags:
- API-v2
patch:
description: |-
The user must either own the message and request the resource with the READ or ADMIN Key
Or the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key
The returned message is never trimmed
operationId: api-messages-get
parameters:
- description: MessageID
in: path
name: mid
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.MessageJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Get a single message (untrimmed)
tags:
- API-v2
/api/v2/users:
post:
operationId: api-user-create
parameters:
@ -1252,7 +1252,7 @@ paths:
summary: Create a new user
tags:
- API-v2
/api/users/{uid}:
/api/v2/users/{uid}:
get:
operationId: api-user-get
parameters:
@ -1343,7 +1343,7 @@ paths:
summary: (Partially) update a user
tags:
- API-v2
/api/users/{uid}/channels:
/api/v2/users/{uid}/channels:
get:
description: |-
The possible values for 'selector' are:
@ -1426,7 +1426,7 @@ paths:
summary: Create a new (empty) channel
tags:
- API-v2
/api/users/{uid}/channels/{cid}:
/api/v2/users/{uid}/channels/{cid}:
get:
operationId: api-channels-get
parameters:
@ -1517,7 +1517,7 @@ paths:
summary: (Partially) update a channel
tags:
- API-v2
/api/users/{uid}/channels/{cid}/messages:
/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
@ -1572,7 +1572,7 @@ paths:
summary: List messages of a channel
tags:
- API-v2
/api/users/{uid}/channels/{cid}/subscriptions:
/api/v2/users/{uid}/channels/{cid}/subscriptions:
get:
operationId: api-chan-subscriptions-list
parameters:
@ -1610,7 +1610,7 @@ paths:
summary: List all subscriptions of a channel
tags:
- API-v2
/api/users/{uid}/clients:
/api/v2/users/{uid}/clients:
get:
operationId: api-clients-list
parameters:
@ -1672,7 +1672,7 @@ paths:
summary: Add a new clients
tags:
- API-v2
/api/users/{uid}/clients/{cid}:
/api/v2/users/{uid}/clients/{cid}:
delete:
operationId: api-clients-delete
parameters:
@ -1747,7 +1747,7 @@ paths:
summary: Get a single client
tags:
- API-v2
/api/users/{uid}/subscriptions:
/api/v2/users/{uid}/subscriptions:
get:
description: |-
The possible values for 'selector' are:
@ -1834,7 +1834,7 @@ paths:
summary: Create/Request a subscription
tags:
- API-v2
/api/users/{uid}/subscriptions/{sid}:
/api/v2/users/{uid}/subscriptions/{sid}:
delete:
operationId: api-subscriptions-delete
parameters:

View File

@ -463,35 +463,6 @@ func TestCompatExpand(t *testing.T) {
}
func TestCompatRequery(t *testing.T) {
_, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
r0 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/register.php?fcm_token=%s&pro=%s&pro_token=%s", "DUMMY_FCM", "0", ""))
tt.AssertEqual(t, "success", true, r0["success"])
userid := int64(r0["user_id"].(float64))
userkey := r0["user_key"].(string)
rq1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq1["success"])
tt.AssertEqual(t, "count", 0, rq1["count"])
tt.AssertStrRepEqual(t, "data", make([]any, 0), rq1["data"])
r1 := tt.RequestPost[gin.H](t, baseUrl, "/send.php", tt.FormData{
"user_id": fmt.Sprintf("%d", userid),
"user_key": userkey,
"title": "_title_",
})
tt.AssertEqual(t, "success", true, r1["success"])
rq2 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq2["success"])
tt.AssertEqual(t, "count", 0, rq2["count"])
tt.AssertStrRepEqual(t, "data", make([]any, 0), rq2["data"])
}
func TestCompatUpdateUserKey(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
@ -587,3 +558,139 @@ func TestCompatUpgrade(t *testing.T) {
tt.AssertEqual(t, "quota_max", 1000, r1["quota_max"])
tt.AssertEqual(t, "is_pro", true, r1["is_pro"])
}
func TestCompatRequery(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
r0 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/register.php?fcm_token=%s&pro=%s&pro_token=%s", "DUMMY_FCM", "0", ""))
tt.AssertEqual(t, "success", true, r0["success"])
userid := int64(r0["user_id"].(float64))
userkey := r0["user_key"].(string)
useridnew := tt.ConvertCompatID(t, ws, userid, "userid")
rq1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq1["success"])
tt.AssertEqual(t, "count", 0, rq1["count"])
tt.AssertStrRepEqual(t, "data", make([]any, 0), rq1["data"])
r1 := tt.RequestPost[gin.H](t, baseUrl, "/send.php", tt.FormData{
"user_id": fmt.Sprintf("%d", userid),
"user_key": userkey,
"title": "_title_",
"msg_id": "r1",
})
tt.AssertEqual(t, "success", true, r1["success"])
type respRequery struct {
Success bool `json:"success"`
Message string `json:"message"`
Count int `json:"count"`
Data []gin.H `json:"data"`
}
rq2 := tt.RequestGet[respRequery](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq2.Success)
tt.AssertEqual(t, "count", 1, rq2.Count)
tt.AssertMappedSet(t, "data", []string{"r1"}, rq2.Data, "usr_msg_id")
rq3 := tt.RequestGet[respRequery](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq3.Success)
tt.AssertEqual(t, "count", 1, rq3.Count)
tt.AssertMappedSet(t, "data", []string{"r1"}, rq3.Data, "usr_msg_id")
a2 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, int(r1["scn_msg_id"].(float64))))
tt.AssertEqual(t, "success", true, a2["success"])
rq31 := tt.RequestGet[respRequery](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq31.Success)
tt.AssertEqual(t, "count", 0, rq31.Count)
r2 := tt.RequestPost[gin.H](t, baseUrl, "/send.php", tt.FormData{
"user_id": fmt.Sprintf("%d", userid),
"user_key": userkey,
"title": "_title_",
"msg_id": "r2",
})
tt.AssertEqual(t, "success", true, r2["success"])
rq4 := tt.RequestGet[respRequery](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq4.Success)
tt.AssertEqual(t, "count", 1, rq4.Count)
tt.AssertMappedSet(t, "data", []string{"r2"}, rq4.Data, "usr_msg_id")
r3 := tt.RequestPost[gin.H](t, baseUrl, "/send.php", tt.FormData{
"user_id": fmt.Sprintf("%d", userid),
"user_key": userkey,
"title": "_title_",
"msg_id": "r3",
})
tt.AssertEqual(t, "success", true, r3["success"])
r4 := tt.RequestPost[gin.H](t, baseUrl, "/send.php", tt.FormData{
"user_id": fmt.Sprintf("%d", userid),
"user_key": userkey,
"title": "_title_",
"msg_id": "r4",
})
tt.AssertEqual(t, "success", true, r4["success"])
r5 := tt.RequestPost[gin.H](t, baseUrl, "/send.php", tt.FormData{
"user_id": fmt.Sprintf("%d", userid),
"user_key": userkey,
"title": "_title_",
"msg_id": "r5",
})
tt.AssertEqual(t, "success", true, r5["success"])
a1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, int(r4["scn_msg_id"].(float64))))
tt.AssertEqual(t, "success", true, a1["success"])
rq5 := tt.RequestGet[respRequery](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq5.Success)
tt.AssertEqual(t, "count", 3, rq5.Count)
tt.AssertMappedSet(t, "data", []string{"r2", "r3", "r5"}, rq5.Data, "usr_msg_id")
a7 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, int(r2["scn_msg_id"].(float64))))
tt.AssertEqual(t, "success", true, a7["success"])
a3 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, int(r3["scn_msg_id"].(float64))))
tt.AssertEqual(t, "success", true, a3["success"])
tt.AssertEqual(t, "prev_ack", 0, a3["prev_ack"])
tt.AssertEqual(t, "new_ack", 1, a3["new_ack"])
a4 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, int(r3["scn_msg_id"].(float64))))
tt.AssertEqual(t, "success", true, a4["success"])
tt.AssertEqual(t, "prev_ack", 1, a4["prev_ack"])
tt.AssertEqual(t, "new_ack", 1, a4["new_ack"])
a5 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, int(r5["scn_msg_id"].(float64))))
tt.AssertEqual(t, "success", true, a5["success"])
tt.AssertEqual(t, "prev_ack", 0, a5["prev_ack"])
tt.AssertEqual(t, "new_ack", 1, a5["new_ack"])
r6 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_id": useridnew,
"user_key": userkey,
"title": "HelloWorld_001",
"msg_id": "r6",
})
tt.AssertEqual(t, "success", true, r6["success"])
rq6 := tt.RequestGet[respRequery](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq6.Success)
tt.AssertEqual(t, "count", 1, rq6.Count)
tt.AssertMappedSet(t, "data", []string{"r6"}, rq6.Data, "usr_msg_id")
a6 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, tt.ConvertToCompatID(t, ws, r6["scn_msg_id"].(string))))
tt.AssertEqual(t, "success", true, a6["success"])
tt.AssertEqual(t, "prev_ack", 0, a6["prev_ack"])
tt.AssertEqual(t, "new_ack", 1, a6["new_ack"])
tt.AssertEqual(t, "message", "ok", a6["message"])
rq7 := tt.RequestGet[respRequery](t, baseUrl, fmt.Sprintf("/api/requery.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "success", true, rq7.Success)
tt.AssertEqual(t, "count", 0, rq7.Count)
}

View File

@ -27,6 +27,27 @@ func ConvertToCompatID(t *testing.T, ws *logic.Application, newid string) int64
return *uidold
}
func ConvertCompatID(t *testing.T, ws *logic.Application, oldid int64, idtype string) string {
ctx := ws.NewSimpleTransactionContext(5 * time.Second)
defer ctx.Cancel()
idnew, err := ws.Database.Primary.ConvertCompatID(ctx, oldid, idtype)
TestFailIfErr(t, err)
if idnew == nil {
TestFail(t, "faile to convert oldid to newid (compat)")
}
err = ctx.CommitTransaction()
if err != nil {
TestFail(t, "failed to commit")
return ""
}
return *idnew
}
func CreateCompatID(t *testing.T, ws *logic.Application, idtype string, newid string) int64 {
ctx := ws.NewSimpleTransactionContext(5 * time.Second)