From 80f3b982d2ebec19480e5cd9727596254b2f90f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sun, 20 Nov 2022 00:19:41 +0100 Subject: [PATCH] ListMessages() --- server/.idea/sqldialects.xml | 2 +- server/README.md | 3 +- server/api/apierr/enums.go | 1 + server/api/handler/api.go | 128 +++- server/db/cursortoken/token.go | 133 ++++ server/db/database.go | 25 +- server/db/messages.go | 37 + server/db/schema/assets.go | 12 + server/db/{ => schema}/schema_1.ddl | 0 server/db/{ => schema}/schema_2.ddl | 0 server/db/{ => schema}/schema_3.ddl | 0 server/db/{ => schema}/schema_sqlite.ddl | 0 server/logic/permissions.go | 12 + server/swagger/swagger.json | 836 +++++++++++++++++++++-- server/swagger/swagger.yaml | 561 ++++++++++++++- 15 files changed, 1635 insertions(+), 115 deletions(-) create mode 100644 server/db/cursortoken/token.go create mode 100644 server/db/schema/assets.go rename server/db/{ => schema}/schema_1.ddl (100%) rename server/db/{ => schema}/schema_2.ddl (100%) rename server/db/{ => schema}/schema_3.ddl (100%) rename server/db/{ => schema}/schema_sqlite.ddl (100%) diff --git a/server/.idea/sqldialects.xml b/server/.idea/sqldialects.xml index 90bf5af..1637558 100644 --- a/server/.idea/sqldialects.xml +++ b/server/.idea/sqldialects.xml @@ -1,7 +1,7 @@ - + diff --git a/server/README.md b/server/README.md index d61360b..01f0c79 100644 --- a/server/README.md +++ b/server/README.md @@ -11,4 +11,5 @@ - List subscriptions on owned channels /RESTful?) - deploy - Dockerfile - - php in html \ No newline at end of file + - php in html + - full-text-search: https://www.sqlite.org/fts5.html#contentless_tables \ No newline at end of file diff --git a/server/api/apierr/enums.go b/server/api/apierr/enums.go index 12abcbf..f78d905 100644 --- a/server/api/apierr/enums.go +++ b/server/api/apierr/enums.go @@ -12,6 +12,7 @@ const ( INVALID_PRIO APIError = 1104 REQ_METHOD APIError = 1105 INVALID_CLIENTTYPE APIError = 1106 + PAGETOKEN_ERROR APIError = 1120 BINDFAIL_QUERY_PARAM APIError = 1151 BINDFAIL_BODY_PARAM APIError = 1152 BINDFAIL_URI_PARAM APIError = 1153 diff --git a/server/api/handler/api.go b/server/api/handler/api.go index dcc0dec..46f8604 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -4,11 +4,13 @@ import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/db" + "blackforestbytes.com/simplecloudnotifier/db/cursortoken" "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" + "gogs.mikescher.com/BlackForestBytes/goext/mathext" "net/http" ) @@ -705,15 +707,15 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { // @Summary Creare/Request a subscription // @ID api-subscriptions-create // -// @Param uid path int true "UserID" -// @Param query_data query handler.CreateSubscription.query false " " -// @Param post_data body handler.CreateSubscription.body false " " +// @Param uid path int true "UserID" +// @Param query_data query handler.CreateSubscription.query false " " +// @Param post_data body handler.CreateSubscription.body false " " // -// @Success 200 {object} models.SubscriptionJSON -// @Failure 400 {object} ginresp.apiError -// @Failure 401 {object} ginresp.apiError -// @Failure 404 {object} ginresp.apiError -// @Failure 500 {object} ginresp.apiError +// @Success 200 {object} models.SubscriptionJSON +// @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}/subscriptions [POST] func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { @@ -820,28 +822,98 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) } +// 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 +// +// @Param query_data query handler.ListMessages.query false " " +// +// @Success 200 {object} handler.ListMessages.response +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/messages [GET] func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse { - //also update last_read - return ginresp.NotImplemented() //TODO + 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 q query + ctx, errResp := h.app.StartRequest(g, nil, &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 + } + + userid := *ctx.GetPermissionUserID() + + 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) + } + + err = h.database.UpdateUserLastRead(ctx, userid) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update last-read", err) + } + + messages, npt, err := h.database.ListMessages(ctx, userid, 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})) } // GetMessage swaggerdoc // -// @Summary Get a single message (untrimmed) +// @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-message-get +// @ID api-messages-get // -// @Param mid path int true "SCNMessageID" +// @Param mid path int true "SCNMessageID" // -// @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/{mid} [PATCH] +// @Router /api-v2/messages/{mid} [PATCH] func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { type uri struct { MessageID int64 `uri:"mid"` @@ -898,19 +970,19 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { // DeleteMessage swaggerdoc // -// @Summary Delete a single message +// @Summary Delete a single message // @Description The user must own the message and request the resource with the ADMIN Key -// @ID api-message-delete +// @ID api-messages-delete // -// @Param mid path int true "SCNMessageID" +// @Param mid path int true "SCNMessageID" // -// @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/{mid} [PATCH] +// @Router /api-v2/messages/{mid} [PATCH] func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { type uri struct { MessageID int64 `uri:"mid"` diff --git a/server/db/cursortoken/token.go b/server/db/cursortoken/token.go new file mode 100644 index 0000000..0ed5d4a --- /dev/null +++ b/server/db/cursortoken/token.go @@ -0,0 +1,133 @@ +package cursortoken + +import ( + "encoding/base32" + "encoding/json" + "errors" + "strings" + "time" +) + +type Mode string + +const ( + CTMStart = "START" + CTMNormal = "NORMAL" + CTMEnd = "END" +) + +type CursorToken struct { + Mode Mode + Timestamp int64 + Id int64 + Direction string +} + +type cursorTokenSerialize struct { + Timestamp *int64 `json:"ts,omitempty"` + Id *int64 `json:"id,omitempty"` + Direction *string `json:"dir,omitempty"` +} + +func Start() CursorToken { + return CursorToken{ + Mode: CTMStart, + Timestamp: 0, + Id: 0, + Direction: "", + } +} + +func End() CursorToken { + return CursorToken{ + Mode: CTMEnd, + Timestamp: 0, + Id: 0, + Direction: "", + } +} + +func Normal(ts time.Time, id int64, dir string) CursorToken { + return CursorToken{ + Mode: CTMNormal, + Timestamp: ts.UnixMilli(), + Id: id, + Direction: dir, + } +} + +func (c *CursorToken) Token() string { + if c.Mode == CTMStart { + return "@start" + } + if c.Mode == CTMEnd { + return "@end" + } + + // We kinda manually implement omitempty for the CursorToken here + // because omitempty does not work for time.Time and otherwise we would always + // get weird time values when decoding a token that initially didn't have an Timestamp set + // For this usecase we treat Unix=0 as an empty timestamp + + sertok := cursorTokenSerialize{} + + if c.Id != 0 { + sertok.Id = &c.Id + } + + if c.Timestamp != 0 { + sertok.Timestamp = &c.Timestamp + } + + if c.Direction != "" { + sertok.Direction = &c.Direction + } + + body, err := json.Marshal(sertok) + if err != nil { + panic(err) + } + + return "tok_" + base32.StdEncoding.EncodeToString(body) +} + +func Decode(tok string) (CursorToken, error) { + if tok == "" { + return Start(), nil + } + if strings.ToLower(tok) == "@start" { + return Start(), nil + } + if strings.ToLower(tok) == "@end" { + return End(), nil + } + + if !strings.HasPrefix(tok, "tok_") { + return CursorToken{}, errors.New("could not decode token, missing prefix") + } + + body, err := base32.StdEncoding.DecodeString(tok[len("tok_"):]) + if err != nil { + return CursorToken{}, err + } + + var tokenDeserialize cursorTokenSerialize + err = json.Unmarshal(body, &tokenDeserialize) + if err != nil { + return CursorToken{}, err + } + + token := CursorToken{Mode: CTMNormal} + + if tokenDeserialize.Timestamp != nil { + token.Timestamp = *tokenDeserialize.Timestamp + } + if tokenDeserialize.Id != nil { + token.Id = *tokenDeserialize.Id + } + if tokenDeserialize.Direction != nil { + token.Direction = *tokenDeserialize.Direction + } + + return token, nil +} diff --git a/server/db/database.go b/server/db/database.go index 3de8c2c..6b885b3 100644 --- a/server/db/database.go +++ b/server/db/database.go @@ -2,24 +2,15 @@ package db import ( scn "blackforestbytes.com/simplecloudnotifier" + "blackforestbytes.com/simplecloudnotifier/db/schema" "context" "database/sql" - _ "embed" "errors" "fmt" _ "github.com/mattn/go-sqlite3" "time" ) -//go:embed schema_1.ddl -var schema1 string - -//go:embed schema_2.ddl -var schema2 string - -//go:embed schema_3.ddl -var schema3 string - type Database struct { db *sql.DB } @@ -37,24 +28,24 @@ func (db *Database) Migrate(ctx context.Context) error { ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second) defer cancel() - schema, err := db.ReadSchema(ctx) - if schema == 0 { + currschema, err := db.ReadSchema(ctx) + if currschema == 0 { - _, err = db.db.ExecContext(ctx, schema3) + _, err = db.db.ExecContext(ctx, schema.Schema3) if err != nil { return err } return nil - } else if schema == 1 { + } else if currschema == 1 { return errors.New("cannot autom. upgrade schema 1") - } else if schema == 2 { + } else if currschema == 2 { return errors.New("cannot autom. upgrade schema 2") //TODO - } else if schema == 3 { + } else if currschema == 3 { return nil // current } else { - return errors.New(fmt.Sprintf("Unknown DB schema: %d", schema)) + return errors.New(fmt.Sprintf("Unknown DB schema: %d", currschema)) } } diff --git a/server/db/messages.go b/server/db/messages.go index ce8326e..8cf051f 100644 --- a/server/db/messages.go +++ b/server/db/messages.go @@ -1,8 +1,10 @@ package db import ( + "blackforestbytes.com/simplecloudnotifier/db/cursortoken" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "fmt" "time" ) @@ -103,3 +105,38 @@ func (db *Database) DeleteMessage(ctx TxContext, scnMessageID int64) error { return nil } + +func (db *Database) ListMessages(ctx TxContext, userid 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 messages.* FROM messages LEFT JOIN subscriptions subs on messages.channel_id = subs.channel_id WHERE subs.subscriber_user_id = ? AND subs.confirmed = 1 "+pageCond+" ORDER BY timestamp_real DESC LIMIT ?", + userid, + 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/db/schema/assets.go b/server/db/schema/assets.go new file mode 100644 index 0000000..97204d6 --- /dev/null +++ b/server/db/schema/assets.go @@ -0,0 +1,12 @@ +package schema + +import _ "embed" + +//go:embed schema_1.ddl +var Schema1 string + +//go:embed schema_2.ddl +var Schema2 string + +//go:embed schema_3.ddl +var Schema3 string diff --git a/server/db/schema_1.ddl b/server/db/schema/schema_1.ddl similarity index 100% rename from server/db/schema_1.ddl rename to server/db/schema/schema_1.ddl diff --git a/server/db/schema_2.ddl b/server/db/schema/schema_2.ddl similarity index 100% rename from server/db/schema_2.ddl rename to server/db/schema/schema_2.ddl diff --git a/server/db/schema_3.ddl b/server/db/schema/schema_3.ddl similarity index 100% rename from server/db/schema_3.ddl rename to server/db/schema/schema_3.ddl diff --git a/server/db/schema_sqlite.ddl b/server/db/schema/schema_sqlite.ddl similarity index 100% rename from server/db/schema_sqlite.ddl rename to server/db/schema/schema_sqlite.ddl diff --git a/server/logic/permissions.go b/server/logic/permissions.go index eb0bf22..190c536 100644 --- a/server/logic/permissions.go +++ b/server/logic/permissions.go @@ -42,6 +42,18 @@ func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPRespons return langext.Ptr(respoNotAuthorized) } +func (ac *AppContext) CheckPermissionRead() *ginresp.HTTPResponse { + p := ac.permissions + if p.UserID != nil && p.KeyType == PermKeyTypeUserRead { + return nil + } + if p.UserID != nil && p.KeyType == PermKeyTypeUserAdmin { + return nil + } + + return langext.Ptr(respoNotAuthorized) +} + func (ac *AppContext) CheckPermissionUserAdmin(userid int64) *ginresp.HTTPResponse { p := ac.permissions if p.UserID != nil && *p.UserID == userid && p.KeyType == PermKeyTypeUserAdmin { diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index c51109c..db559ef 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -11,16 +11,22 @@ "paths": { "/": { "post": { + "description": "All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required", "summary": "Send a new message", "parameters": [ { "type": "string", - "name": "message_content", + "name": "chanKey", "in": "query" }, { "type": "string", - "name": "message_title", + "name": "channel", + "in": "query" + }, + { + "type": "string", + "name": "content", "in": "query" }, { @@ -29,25 +35,30 @@ "in": "query" }, { - "type": "integer", + "type": "number", "name": "sendTimestamp", "in": "query" }, + { + "type": "string", + "name": "title", + "in": "query" + }, + { + "type": "integer", + "name": "userID", + "in": "query" + }, + { + "type": "string", + "name": "userKey", + "in": "query" + }, { "type": "string", "name": "userMessageID", "in": "query" }, - { - "type": "string", - "name": "user_id", - "in": "query" - }, - { - "type": "string", - "name": "user_key", - "in": "query" - }, { "description": " ", "name": "post_body", @@ -61,7 +72,122 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.ClientJSON" + "$ref": "#/definitions/handler.SendMessage.response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "403": { + "description": "Forbidden", + "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": { + "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 all (subscribed) messages", + "operationId": "api-messages-list", + "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.ListMessages.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/messages/{mid}": { + "patch": { + "description": "The user must own the message and request the resource with the ADMIN Key", + "summary": "Delete a single message", + "operationId": "api-messages-delete", + "parameters": [ + { + "type": "integer", + "description": "SCNMessageID", + "name": "mid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MessageJSON" } }, "400": { @@ -221,6 +347,161 @@ } } }, + "/api-v2/users/{uid}/channels": { + "get": { + "summary": "List all channels of a user", + "operationId": "api-channels-list", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ListChannels.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}": { + "get": { + "summary": "List all channels of a user", + "operationId": "api-channels-get", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "ChannelID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ChannelJSON" + } + }, + "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", + "operationId": "api-chan-subscriptions-list", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "ChannelID", + "name": "cid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ListChannelSubscriptions.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}/clients": { "get": { "summary": "List all clients", @@ -238,7 +519,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.ListClients.result" + "$ref": "#/definitions/handler.ListClients.response" } }, "400": { @@ -374,6 +655,269 @@ } } }, + "/api-v2/users/{uid}/subscriptions": { + "get": { + "summary": "List all channels of a user", + "operationId": "api-user-subscriptions-list", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ListUserSubscriptions.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" + } + } + } + }, + "post": { + "summary": "Creare/Request a subscription", + "operationId": "api-subscriptions-create", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "chanSubscribeKey", + "in": "query" + }, + { + "description": " ", + "name": "post_data", + "in": "body", + "schema": { + "$ref": "#/definitions/handler.CreateSubscription.body" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.SubscriptionJSON" + } + }, + "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}/subscriptions/{sid}": { + "get": { + "summary": "Get a single subscription", + "operationId": "api-subscriptions-get", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "SubscriptionID", + "name": "sid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.SubscriptionJSON" + } + }, + "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" + } + } + } + }, + "delete": { + "summary": "Cancel (delete) subscription", + "operationId": "api-subscriptions-delete", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "SubscriptionID", + "name": "sid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.SubscriptionJSON" + } + }, + "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" + } + } + } + }, + "patch": { + "summary": "Update a subscription (e.g. confirm)", + "operationId": "api-subscriptions-update", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "SubscriptionID", + "name": "sid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.SubscriptionJSON" + } + }, + "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/ack.php": { "get": { "summary": "Acknowledge that a message was received", @@ -773,16 +1317,22 @@ }, "/send": { "post": { + "description": "All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required", "summary": "Send a new message", "parameters": [ { "type": "string", - "name": "message_content", + "name": "chanKey", "in": "query" }, { "type": "string", - "name": "message_title", + "name": "channel", + "in": "query" + }, + { + "type": "string", + "name": "content", "in": "query" }, { @@ -791,25 +1341,30 @@ "in": "query" }, { - "type": "integer", + "type": "number", "name": "sendTimestamp", "in": "query" }, + { + "type": "string", + "name": "title", + "in": "query" + }, + { + "type": "integer", + "name": "userID", + "in": "query" + }, + { + "type": "string", + "name": "userKey", + "in": "query" + }, { "type": "string", "name": "userMessageID", "in": "query" }, - { - "type": "string", - "name": "user_id", - "in": "query" - }, - { - "type": "string", - "name": "user_key", - "in": "query" - }, { "description": " ", "name": "post_body", @@ -823,7 +1378,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.ClientJSON" + "$ref": "#/definitions/handler.SendMessage.response" } }, "400": { @@ -838,6 +1393,12 @@ "$ref": "#/definitions/ginresp.apiError" } }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, "404": { "description": "Not Found", "schema": { @@ -907,6 +1468,17 @@ } } }, + "handler.CreateSubscription.body": { + "type": "object", + "properties": { + "channel": { + "type": "string" + }, + "channelOwnerUserID": { + "type": "integer" + } + } + }, "handler.CreateUser.body": { "type": "object", "properties": { @@ -1001,7 +1573,29 @@ } } }, - "handler.ListClients.result": { + "handler.ListChannelSubscriptions.response": { + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "$ref": "#/definitions/models.SubscriptionJSON" + } + } + } + }, + "handler.ListChannels.response": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ChannelJSON" + } + } + } + }, + "handler.ListClients.response": { "type": "object", "properties": { "clients": { @@ -1012,6 +1606,34 @@ } } }, + "handler.ListMessages.response": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MessageJSON" + } + }, + "next_page_token": { + "type": "string" + }, + "page_size": { + "type": "integer" + } + } + }, + "handler.ListUserSubscriptions.response": { + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "$ref": "#/definitions/models.SubscriptionJSON" + } + } + } + }, "handler.Register.response": { "type": "object", "properties": { @@ -1061,29 +1683,70 @@ "handler.SendMessage.body": { "type": "object", "properties": { + "chanKey": { + "type": "string" + }, + "channel": { + "type": "string" + }, "message_content": { "type": "string" }, "message_title": { "type": "string" }, + "msg_id": { + "type": "string" + }, "priority": { "type": "integer" }, - "sendTimestamp": { - "type": "integer" - }, - "userMessageID": { - "type": "string" + "timestamp": { + "type": "number" }, "user_id": { - "type": "string" + "type": "integer" }, "user_key": { "type": "string" } } }, + "handler.SendMessage.response": { + "type": "object", + "properties": { + "errhighlight": { + "type": "integer" + }, + "error": { + "type": "integer" + }, + "is_pro": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "messagecount": { + "type": "integer" + }, + "quota": { + "type": "integer" + }, + "quota_max": { + "type": "integer" + }, + "scn_msg_id": { + "type": "integer" + }, + "success": { + "type": "boolean" + }, + "suppress_send": { + "type": "boolean" + } + } + }, "handler.Update.response": { "type": "object", "properties": { @@ -1172,6 +1835,35 @@ } } }, + "models.ChannelJSON": { + "type": "object", + "properties": { + "channel_id": { + "type": "integer" + }, + "messages_sent": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "owner_user_id": { + "type": "integer" + }, + "send_key": { + "type": "string" + }, + "subscribe_key": { + "type": "string" + }, + "timestamp_created": { + "type": "string" + }, + "timestamp_last_sent": { + "type": "string" + } + } + }, "models.ClientJSON": { "type": "object", "properties": { @@ -1221,6 +1913,44 @@ } } }, + "models.MessageJSON": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "channel_id": { + "type": "integer" + }, + "channel_name": { + "type": "string" + }, + "owner_user_id": { + "type": "integer" + }, + "priority": { + "type": "integer" + }, + "scn_message_id": { + "type": "integer" + }, + "sender_user_id": { + "type": "integer" + }, + "timestamp": { + "type": "string" + }, + "title": { + "type": "string" + }, + "trimmed": { + "type": "boolean" + }, + "usr_message_id": { + "type": "string" + } + } + }, "models.ShortCompatMessage": { "type": "object", "properties": { @@ -1247,6 +1977,32 @@ } } }, + "models.SubscriptionJSON": { + "type": "object", + "properties": { + "channel_id": { + "type": "integer" + }, + "channel_name": { + "type": "string" + }, + "channel_owner_user_id": { + "type": "integer" + }, + "confirmed": { + "type": "boolean" + }, + "subscriber_user_id": { + "type": "integer" + }, + "subscription_id": { + "type": "integer" + }, + "timestamp_created": { + "type": "string" + } + } + }, "models.UserJSON": { "type": "object", "properties": { @@ -1259,12 +2015,12 @@ "messages_sent": { "type": "integer" }, - "quota_day": { - "type": "string" - }, - "quota_today": { + "quota_used": { "type": "integer" }, + "quota_used_day": { + "type": "string" + }, "read_key": { "type": "string" }, diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index f34f37f..4319586 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -34,6 +34,13 @@ definitions: fcm_token: type: string type: object + handler.CreateSubscription.body: + properties: + channel: + type: string + channelOwnerUserID: + type: integer + type: object handler.CreateUser.body: properties: agent_model: @@ -95,13 +102,45 @@ definitions: user_key: type: string type: object - handler.ListClients.result: + handler.ListChannelSubscriptions.response: + properties: + subscriptions: + items: + $ref: '#/definitions/models.SubscriptionJSON' + type: array + type: object + handler.ListChannels.response: + properties: + channels: + items: + $ref: '#/definitions/models.ChannelJSON' + type: array + type: object + handler.ListClients.response: properties: clients: items: $ref: '#/definitions/models.ClientJSON' type: array type: object + handler.ListMessages.response: + properties: + messages: + items: + $ref: '#/definitions/models.MessageJSON' + type: array + next_page_token: + type: string + page_size: + type: integer + type: object + handler.ListUserSubscriptions.response: + properties: + subscriptions: + items: + $ref: '#/definitions/models.SubscriptionJSON' + type: array + type: object handler.Register.response: properties: is_pro: @@ -134,20 +173,47 @@ definitions: type: object handler.SendMessage.body: properties: + chanKey: + type: string + channel: + type: string message_content: type: string message_title: type: string + msg_id: + type: string priority: type: integer - sendTimestamp: - type: integer + timestamp: + type: number user_id: - type: string + type: integer user_key: type: string - userMessageID: + type: object + handler.SendMessage.response: + properties: + errhighlight: + type: integer + error: + type: integer + is_pro: + type: boolean + message: type: string + messagecount: + type: integer + quota: + type: integer + quota_max: + type: integer + scn_msg_id: + type: integer + success: + type: boolean + suppress_send: + type: boolean type: object handler.Update.response: properties: @@ -206,6 +272,25 @@ definitions: uri: type: string type: object + models.ChannelJSON: + properties: + channel_id: + type: integer + messages_sent: + type: integer + name: + type: string + owner_user_id: + type: integer + send_key: + type: string + subscribe_key: + type: string + timestamp_created: + type: string + timestamp_last_sent: + type: string + type: object models.ClientJSON: properties: agent_model: @@ -238,6 +323,31 @@ definitions: usr_msg_id: type: string type: object + models.MessageJSON: + properties: + body: + type: string + channel_id: + type: integer + channel_name: + type: string + owner_user_id: + type: integer + priority: + type: integer + scn_message_id: + type: integer + sender_user_id: + type: integer + timestamp: + type: string + title: + type: string + trimmed: + type: boolean + usr_message_id: + type: string + type: object models.ShortCompatMessage: properties: body: @@ -255,6 +365,23 @@ definitions: usr_msg_id: type: string type: object + models.SubscriptionJSON: + properties: + channel_id: + type: integer + channel_name: + type: string + channel_owner_user_id: + type: integer + confirmed: + type: boolean + subscriber_user_id: + type: integer + subscription_id: + type: integer + timestamp_created: + type: string + type: object models.UserJSON: properties: admin_key: @@ -263,10 +390,10 @@ definitions: type: boolean messages_sent: type: integer - quota_day: - type: string - quota_today: + quota_used: type: integer + quota_used_day: + type: string read_key: type: string send_key: @@ -291,28 +418,36 @@ info: paths: /: post: + description: All parameter can be set via query-parameter or the json body. + Only UserID, UserKey and Title are required parameters: - in: query - name: message_content + name: chanKey type: string - in: query - name: message_title + name: channel + type: string + - in: query + name: content type: string - in: query name: priority type: integer - in: query name: sendTimestamp + type: number + - in: query + name: title + type: string + - in: query + name: userID type: integer + - in: query + name: userKey + type: string - in: query name: userMessageID type: string - - in: query - name: user_id - type: string - - in: query - name: user_key - type: string - description: ' ' in: body name: post_body @@ -322,7 +457,54 @@ paths: "200": description: OK schema: - $ref: '#/definitions/models.ClientJSON' + $ref: '#/definitions/handler.SendMessage.response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ginresp.apiError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/ginresp.apiError' + "403": + description: Forbidden + 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: Send a new message + /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: nextPageToken + type: string + - in: query + name: pageSize + type: integer + - in: query + name: trimmed + type: boolean + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ListMessages.response' "400": description: Bad Request schema: @@ -339,7 +521,40 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/ginresp.apiError' - summary: Send a new message + summary: List all (subscribed) messages + /api-v2/messages/{mid}: + patch: + description: The user must own the message and request the resource with the + ADMIN Key + operationId: api-messages-delete + parameters: + - description: SCNMessageID + in: path + name: mid + required: true + type: integer + 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: Delete a single message /api-v2/users/: post: operationId: api-user-create @@ -425,6 +640,109 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' summary: (Partially) update a user + /api-v2/users/{uid}/channels: + get: + operationId: api-channels-list + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ListChannels.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 all channels of a user + /api-v2/users/{uid}/channels/{cid}: + get: + operationId: api-channels-get + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + - description: ChannelID + in: path + name: cid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ChannelJSON' + "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 all channels of a user + /api-v2/users/{uid}/channels/{cid}/subscriptions: + get: + operationId: api-chan-subscriptions-list + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + - description: ChannelID + in: path + name: cid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ListChannelSubscriptions.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 all subscriptions of a channel /api-v2/users/{uid}/clients: get: operationId: api-clients-list @@ -438,7 +756,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handler.ListClients.result' + $ref: '#/definitions/handler.ListClients.response' "400": description: Bad Request schema: @@ -527,6 +845,181 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' summary: Get a single clients + /api-v2/users/{uid}/subscriptions: + get: + operationId: api-user-subscriptions-list + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ListUserSubscriptions.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 all channels of a user + post: + operationId: api-subscriptions-create + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + - in: query + name: chanSubscribeKey + type: string + - description: ' ' + in: body + name: post_data + schema: + $ref: '#/definitions/handler.CreateSubscription.body' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.SubscriptionJSON' + "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: Creare/Request a subscription + /api-v2/users/{uid}/subscriptions/{sid}: + delete: + operationId: api-subscriptions-delete + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + - description: SubscriptionID + in: path + name: sid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.SubscriptionJSON' + "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: Cancel (delete) subscription + get: + operationId: api-subscriptions-get + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + - description: SubscriptionID + in: path + name: sid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.SubscriptionJSON' + "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: Get a single subscription + patch: + operationId: api-subscriptions-update + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + - description: SubscriptionID + in: path + name: sid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.SubscriptionJSON' + "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: Update a subscription (e.g. confirm) /api/ack.php: get: operationId: compat-ack @@ -790,28 +1283,36 @@ paths: $ref: '#/definitions/ginresp.apiError' /send: post: + description: All parameter can be set via query-parameter or the json body. + Only UserID, UserKey and Title are required parameters: - in: query - name: message_content + name: chanKey type: string - in: query - name: message_title + name: channel + type: string + - in: query + name: content type: string - in: query name: priority type: integer - in: query name: sendTimestamp + type: number + - in: query + name: title + type: string + - in: query + name: userID type: integer + - in: query + name: userKey + type: string - in: query name: userMessageID type: string - - in: query - name: user_id - type: string - - in: query - name: user_key - type: string - description: ' ' in: body name: post_body @@ -821,7 +1322,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/models.ClientJSON' + $ref: '#/definitions/handler.SendMessage.response' "400": description: Bad Request schema: @@ -830,6 +1331,10 @@ paths: description: Unauthorized schema: $ref: '#/definitions/ginresp.apiError' + "403": + description: Forbidden + schema: + $ref: '#/definitions/ginresp.apiError' "404": description: Not Found schema: