diff --git a/server/Makefile b/server/Makefile index 9962ec1..2cbb343 100644 --- a/server/Makefile +++ b/server/Makefile @@ -5,7 +5,7 @@ PORT=9090 NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD) HASH=$(shell git rev-parse HEAD) -build: +build: swagger fmt rm -f ./_build/scn_backend CGO_ENABLED=1 go build -v -o _build/scn_backend -tags timetzdata ./cmd/scnserver @@ -13,7 +13,7 @@ run: build mkdir -p .run-data _build/scn_backend -docker: +docker: build [ ! -f "DOCKER_GIT_INFO" ] || rm DOCKER_GIT_INFO git rev-parse --abbrev-ref HEAD >> DOCKER_GIT_INFO git rev-parse HEAD >> DOCKER_GIT_INFO @@ -33,7 +33,7 @@ swagger: which swag || go install github.com/swaggo/swag/cmd/swag@latest swag init -generalInfo api/router.go --output ./swagger/ --outputTypes "json,yaml" -run-docker-local: +run-docker-local: docker mkdir -p .run-data docker run --rm \ --init \ @@ -42,7 +42,7 @@ run-docker-local: --publish "8080:80" \ $(DOCKER_NAME):latest -inspect-docker: +inspect-docker: docker mkdir -p .run-data docker run -ti \ --rm \ @@ -50,7 +50,7 @@ inspect-docker: $(DOCKER_NAME):latest \ bash -push-docker: +push-docker: docker docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(HASH)" docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest" docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):latest" diff --git a/server/README.md b/server/README.md index 7017727..8e48ea2 100644 --- a/server/README.md +++ b/server/README.md @@ -2,6 +2,8 @@ //TODO + - remove fcm/goog_api keys from repo (and invalidate them !!) + - migration script for existing data - ack/read deliveries && return ack-count (? or not, how to query?) @@ -9,8 +11,8 @@ - verify pro_token - full-text-search: https://www.sqlite.org/fts5.html#contentless_tables - - update html (--channel, api doku, better send route, etc) - dark mode toggle for html + - app-store link in HTML - deploy diff --git a/server/api/handler/api.go b/server/api/handler/api.go index c0712f8..50291e8 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -1356,6 +1356,9 @@ func (h APIHandler) CreateMessage(g *gin.Context) ginresp.HTTPResponse { if b.Content != nil && len(*b.Content) > user.MaxContentLength() { return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*b.Content), user.MaxContentLength()), nil) } + if len(channelName) > user.MaxChannelNameLength() { + return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, 106, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) + } if b.UserMessageID != nil { msg, err := h.database.GetMessageByUserMessageID(ctx, *b.UserMessageID) diff --git a/server/api/handler/message.go b/server/api/handler/message.go index 590d75e..a620de6 100644 --- a/server/api/handler/message.go +++ b/server/api/handler/message.go @@ -44,7 +44,6 @@ func NewMessageHandler(app *logic.Application) MessageHandler { // @Failure 400 {object} ginresp.apiError // @Failure 401 {object} ginresp.apiError // @Failure 403 {object} ginresp.apiError -// @Failure 404 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError // // @Router /send.php [POST] @@ -93,10 +92,9 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse { // // @Success 200 {object} handler.sendMessageInternal.response // @Failure 400 {object} ginresp.apiError -// @Failure 401 {object} ginresp.apiError -// @Failure 403 {object} ginresp.apiError -// @Failure 404 {object} ginresp.apiError -// @Failure 500 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError "The user_id was not found or the user_key is wrong" +// @Failure 403 {object} ginresp.apiError "The user has exceeded its daily quota - wait 24 hours or upgrade your account" +// @Failure 500 {object} ginresp.apiError "An internal server error occurred - try again later" // // @Router / [POST] // @Router /send [POST] @@ -212,6 +210,9 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex if Content != nil && len(*Content) > user.MaxContentLength() { return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil) } + if len(channelName) > user.MaxChannelNameLength() { + return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, 106, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) + } if UserMessageID != nil { msg, err := h.database.GetMessageByUserMessageID(ctx, *UserMessageID) diff --git a/server/api/handler/website.go b/server/api/handler/website.go index b93d539..0df6148 100644 --- a/server/api/handler/website.go +++ b/server/api/handler/website.go @@ -4,43 +4,47 @@ import ( "blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/website" + "errors" "github.com/gin-gonic/gin" "net/http" + "regexp" "strings" ) type WebsiteHandler struct { - app *logic.Application + app *logic.Application + rexTemplate *regexp.Regexp } func NewWebsiteHandler(app *logic.Application) WebsiteHandler { return WebsiteHandler{ - app: app, + app: app, + rexTemplate: regexp.MustCompile("{{template\\|[A-Za-z0-9_\\-.]+}}"), } } func (h WebsiteHandler) Index(g *gin.Context) ginresp.HTTPResponse { - return h.serveAsset(g, "index.html") + return h.serveAsset(g, "index.html", true) } func (h WebsiteHandler) APIDocs(g *gin.Context) ginresp.HTTPResponse { - return h.serveAsset(g, "api.html") + return h.serveAsset(g, "api.html", true) } func (h WebsiteHandler) APIDocsMore(g *gin.Context) ginresp.HTTPResponse { - return h.serveAsset(g, "api_more.html") + return h.serveAsset(g, "api_more.html", true) } func (h WebsiteHandler) MessageSent(g *gin.Context) ginresp.HTTPResponse { - return h.serveAsset(g, "message_sent.html") + return h.serveAsset(g, "message_sent.html", true) } func (h WebsiteHandler) FaviconIco(g *gin.Context) ginresp.HTTPResponse { - return h.serveAsset(g, "favicon.ico") + return h.serveAsset(g, "favicon.ico", false) } func (h WebsiteHandler) FaviconPNG(g *gin.Context) ginresp.HTTPResponse { - return h.serveAsset(g, "favicon.png") + return h.serveAsset(g, "favicon.png", false) } func (h WebsiteHandler) Javascript(g *gin.Context) ginresp.HTTPResponse { @@ -53,7 +57,7 @@ func (h WebsiteHandler) Javascript(g *gin.Context) ginresp.HTTPResponse { return ginresp.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } - return h.serveAsset(g, "js/"+u.Filename) + return h.serveAsset(g, "js/"+u.Filename, false) } func (h WebsiteHandler) CSS(g *gin.Context) ginresp.HTTPResponse { @@ -66,15 +70,32 @@ func (h WebsiteHandler) CSS(g *gin.Context) ginresp.HTTPResponse { return ginresp.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } - return h.serveAsset(g, "css/"+u.Filename) + return h.serveAsset(g, "css/"+u.Filename, false) } -func (h WebsiteHandler) serveAsset(g *gin.Context, fn string) ginresp.HTTPResponse { +func (h WebsiteHandler) serveAsset(g *gin.Context, fn string, repl bool) ginresp.HTTPResponse { data, err := website.Assets.ReadFile(fn) if err != nil { return ginresp.Status(http.StatusNotFound) } + if repl { + failed := false + data = h.rexTemplate.ReplaceAllFunc(data, func(match []byte) []byte { + prefix := len("{{template|") + suffix := len("}}") + fnSub := match[prefix : len(match)-suffix] + subdata, err := website.Assets.ReadFile(string(fnSub)) + if err != nil { + failed = true + } + return subdata + }) + if failed { + return ginresp.InternalError(errors.New("template replacement failed")) + } + } + mime := "text/plain" lowerFN := strings.ToLower(fn) diff --git a/server/config.go b/server/config.go index 4078f91..e8bf45d 100644 --- a/server/config.go +++ b/server/config.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "github.com/rs/zerolog/log" "os" "time" @@ -14,8 +15,8 @@ type Config struct { DBFile string RequestTimeout time.Duration ReturnRawErrors bool - FirebaseProjectID string FirebaseTokenURI string + FirebaseProjectID string FirebasePrivKeyID string FirebaseClientMail string FirebasePrivateKey string @@ -31,11 +32,11 @@ var configLocHost = Config{ DBFile: ".run-data/db.sqlite3", RequestTimeout: 16 * time.Second, ReturnRawErrors: true, - FirebaseProjectID: "simplecloudnotifier-ea7ef", FirebaseTokenURI: "https://oauth2.googleapis.com/token", - FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", - FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", - FirebasePrivateKey: "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQD2NWOQDcalRdkp\nHtQHABLlu3GMBQBJrGiCxzOZhi/lLwrw2MJEmg1VFz6TVkX2z3SCzXCPOgGriM70\nuWCNLyZQvUng7u6/WH9hlpCg0vJpkw6BvOBt1zYu3gbb5M0SKEOR+lDVccEjAnT4\nexebXdJHJcbaYAcPnBQ9tgP+cozQBnr2EfxYL0bGMgiH9fErJSGMBDFI996uUW9a\nbtfkZ/XpZqYAvyGQMEjknGnQ8t8PHAnsS9dc1PXSWfBvz07ba3fkypWcpTsIYUiZ\nSpwTLV8awihKHJuphoTWb4x6p/ijop05qr1p3fe8gZd9qOGgALe+JT4IBLgNYKrP\nLMSKH3TdAgMBAAECggEAdFcWDOP1kfNHgl7G4efvBg9kwD08vZNybxmiEFGQIEPy\nb4x9f90rn6G0N/r0ZIPzEjvxjDxkvaGP6aQPM6er+0r2tgsxVcmDp6F2Bgin86tB\nl5ygkEa5m7vekdmz7XiJNVmLCNEP6nMmwqOnrArRaj03kcj+jSm7hs2TZZDLaSA5\nf+2q7h0jaU7Nm0ZwCNJqfPJEGdu1J3fR29Ej0rI8N0w/BuYRet1VYDO09lquqOPS\n0WirOOWV6eyqijqRT+RCt0vVzAppS6guhN7J7RS0V9GLJ/13sdvHuJy/WTjBb7gQ\na6QTo8D3yYF+cn3+0BmgP55uW7N6tsYwXIRZcTI3IQKBgQD+tDKMx0puZu+8zTX9\nC2oHSb4Frl2xq17ZpbkfFmOBPWfQbAHNiQTUoQlzCOQM6QejykXFvfsddP7EY2tL\npgLUrBh81wSCAOOo19vYwQB3YKa5ZZucKxh2VxFSefL/+BYHijFb0mWBj5HmqWS6\n7l6IYT3L04aRK9kxj0Cg6L/z6wKBgQD3dh/kQlPemfdxRpZUJ6WEE5x3Bv7WjLop\nnWgE02Pk8+DB+s50GD3nOR276ADCYS6OkBsgfMkwhhKWZigiEoK9DMul5n587jc9\no5AalZN3IbBGAoXk+u3g1GC9bOY3454K6IJyhehDTImEFyfm00qfUL8fMNcdEx8O\nnwxtyRawVwKBgGqsnd9IOGw0wIOajtoERcv3npZSiPs4guk092uFvPcL+MbZ9YdX\ns6Y6K/L57klZ79ExjjdbcijML0ehO/ba+KSJz1e51jF8ndzBS1pkuwVEfY94dsvZ\nYM1vednJKXT7On696h5C6DBzKPAqUf3Yh88mqvMLDHkQnE6daLv7vykxAoGAOPmA\ndDx1NO48E1+OIwgRyqv9PUZmDB3Qit5L4biN6lvgJqlJOV+PeRokZ2wOKLLZVkeF\nh2BTrhFgXDJfESEz6rT0eljsTHVIUK/E8On5Ttd5z1SrYUII3NfpAhP9mWaVr6tC\nxX1hMYWAr+Ho9PM23iFoL5U+IdqSLvqdkPVYfPcCgYB1ANKNYPIJNx/wLxYWNS0r\nI98HwKfv2TxxE/l+2459NMMHY5wlpFl7MNoeK2SdY+ghWPlxC6u5Nxpnk+bZ8TJe\np7U2nY0SQDLCmPgGWs3KBb/zR49X2b7JS3CXXqQSrLxBe2phZg6kE5nB6NPUDc/i\n6WG8tG20rCfgwlXeXl0+Ow==\n-----END PRIVATE KEY-----\n", + FirebaseProjectID: "", + FirebasePrivKeyID: "", + FirebaseClientMail: "", + FirebasePrivateKey: "", } var configLocDocker = Config{ @@ -46,11 +47,11 @@ var configLocDocker = Config{ DBFile: "/data/scn_docker.sqlite3", RequestTimeout: 16 * time.Second, ReturnRawErrors: true, - FirebaseProjectID: "simplecloudnotifier-ea7ef", FirebaseTokenURI: "https://oauth2.googleapis.com/token", - FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", - FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", - FirebasePrivateKey: "TODO", + FirebaseProjectID: "", + FirebasePrivKeyID: "", + FirebaseClientMail: "", + FirebasePrivateKey: "", } var configDev = Config{ @@ -61,11 +62,11 @@ var configDev = Config{ DBFile: "/data/scn.sqlite3", RequestTimeout: 16 * time.Second, ReturnRawErrors: true, - FirebaseProjectID: "simplecloudnotifier-ea7ef", FirebaseTokenURI: "https://oauth2.googleapis.com/token", - FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", - FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", - FirebasePrivateKey: "TODO", + FirebaseProjectID: confEnv("FB_PROJECTID"), + FirebasePrivKeyID: confEnv("FB_PRIVATEKEYID"), + FirebaseClientMail: confEnv("FB_CLIENTEMAIL"), + FirebasePrivateKey: confEnv("FB_PRIVATEKEY"), } var configStag = Config{ @@ -76,11 +77,11 @@ var configStag = Config{ DBFile: "/data/scn.sqlite3", RequestTimeout: 16 * time.Second, ReturnRawErrors: true, - FirebaseProjectID: "simplecloudnotifier-ea7ef", FirebaseTokenURI: "https://oauth2.googleapis.com/token", - FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", - FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", - FirebasePrivateKey: "TODO", + FirebaseProjectID: confEnv("FB_PROJECTID"), + FirebasePrivKeyID: confEnv("FB_PRIVATEKEYID"), + FirebaseClientMail: confEnv("FB_CLIENTEMAIL"), + FirebasePrivateKey: confEnv("FB_PRIVATEKEY"), } var configProd = Config{ @@ -91,11 +92,11 @@ var configProd = Config{ DBFile: "/data/scn.sqlite3", RequestTimeout: 16 * time.Second, ReturnRawErrors: false, - FirebaseProjectID: "simplecloudnotifier-ea7ef", FirebaseTokenURI: "https://oauth2.googleapis.com/token", - FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", - FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", - FirebasePrivateKey: "TODO", + FirebaseProjectID: confEnv("FB_PROJECTID"), + FirebasePrivKeyID: confEnv("FB_PRIVATEKEYID"), + FirebaseClientMail: confEnv("FB_CLIENTEMAIL"), + FirebasePrivateKey: confEnv("FB_PRIVATEKEY"), } var allConfig = []Config{ @@ -118,6 +119,14 @@ func getConfig(ns string) (Config, bool) { return Config{}, false } +func confEnv(key string) string { + if v, ok := os.LookupEnv(key); ok { + return v + } else { + panic(fmt.Sprintf("Missing required environment variable '%s'", key)) + } +} + func init() { ns := os.Getenv("CONF_NS") diff --git a/server/models/user.go b/server/models/user.go index 6507f86..25f085a 100644 --- a/server/models/user.go +++ b/server/models/user.go @@ -79,6 +79,10 @@ func (u User) DefaultChannel() string { return "main" } +func (u User) MaxChannelNameLength() int { + return 120 +} + type UserJSON struct { UserID UserID `json:"user_id"` Username *string `json:"username"` diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index 97274d1..a47740c 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -127,25 +127,19 @@ } }, "401": { - "description": "Unauthorized", + "description": "The user_id was not found or the user_key is wrong", "schema": { "$ref": "#/definitions/ginresp.apiError" } }, "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/ginresp.apiError" - } - }, - "404": { - "description": "Not Found", + "description": "The user has exceeded its daily quota - wait 24 hours or upgrade your account", "schema": { "$ref": "#/definitions/ginresp.apiError" } }, "500": { - "description": "Internal Server Error", + "description": "An internal server error occurred - try again later", "schema": { "$ref": "#/definitions/ginresp.apiError" } @@ -1871,25 +1865,19 @@ } }, "401": { - "description": "Unauthorized", + "description": "The user_id was not found or the user_key is wrong", "schema": { "$ref": "#/definitions/ginresp.apiError" } }, "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/ginresp.apiError" - } - }, - "404": { - "description": "Not Found", + "description": "The user has exceeded its daily quota - wait 24 hours or upgrade your account", "schema": { "$ref": "#/definitions/ginresp.apiError" } }, "500": { - "description": "Internal Server Error", + "description": "An internal server error occurred - try again later", "schema": { "$ref": "#/definitions/ginresp.apiError" } @@ -1999,12 +1987,6 @@ "$ref": "#/definitions/ginresp.apiError" } }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/ginresp.apiError" - } - }, "500": { "description": "Internal Server Error", "schema": { diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index 4051d90..8997793 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -514,19 +514,16 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' "401": - description: Unauthorized + description: The user_id was not found or the user_key is wrong schema: $ref: '#/definitions/ginresp.apiError' "403": - description: Forbidden - schema: - $ref: '#/definitions/ginresp.apiError' - "404": - description: Not Found + description: The user has exceeded its daily quota - wait 24 hours or upgrade + your account schema: $ref: '#/definitions/ginresp.apiError' "500": - description: Internal Server Error + description: An internal server error occurred - try again later schema: $ref: '#/definitions/ginresp.apiError' summary: Send a new message @@ -1687,19 +1684,16 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' "401": - description: Unauthorized + description: The user_id was not found or the user_key is wrong schema: $ref: '#/definitions/ginresp.apiError' "403": - description: Forbidden - schema: - $ref: '#/definitions/ginresp.apiError' - "404": - description: Not Found + description: The user has exceeded its daily quota - wait 24 hours or upgrade + your account schema: $ref: '#/definitions/ginresp.apiError' "500": - description: Internal Server Error + description: An internal server error occurred - try again later schema: $ref: '#/definitions/ginresp.apiError' summary: Send a new message @@ -1768,10 +1762,6 @@ paths: description: Forbidden schema: $ref: '#/definitions/ginresp.apiError' - "404": - description: Not Found - schema: - $ref: '#/definitions/ginresp.apiError' "500": description: Internal Server Error schema: diff --git a/server/website/api.html b/server/website/api.html index 8e3f4b8..77d42b5 100644 --- a/server/website/api.html +++ b/server/website/api.html @@ -19,22 +19,27 @@
- + API Documentation + Send

Simple Cloud Notifier

Get your user-id and user-key from the app and send notifications to your phone by performing a POST request against https://simplecloudnotifier.blackforestbytes.com/

-
curl                                          \
-    --data "user_id={userid}"                 \
-    --data "user_key={userkey}"               \
-    --data "title={message_title}"            \
-    --data "content={message_body}"           \
-    --data "priority={0|1|2}"                 \
-    --data "msg_id={unique_message_id}"       \
+        
+curl                                           \
+    --data "user_id=${userid}"                 \
+    --data "user_key=${userkey}"               \
+    --data "title=${message_title}"            \
+    --data "content=${message_body}"           \
+    --data "priority=${0|1|2}"                 \
+    --data "msg_id=$(uuidgen)"                 \
+    --data "timestamp=$(date +%s)"             \
+    --data "channel={channel_name}"            \
     https://scn.blackforestbytes.com/
-

The content, priority and msg_id parameters are optional, you can also send message with only a title and the default priority

-
curl                                          \
+    

Most parameters are optional, you can send a message with only a title (default priority and channel will be used)

+
+curl                                          \
     --data "user_id={userid}"                 \
     --data "user_key={userkey}"               \
     --data "title={message_title}"            \
diff --git a/server/website/api_more.html b/server/website/api_more.html
index f86c54a..0dfde2a 100644
--- a/server/website/api_more.html
+++ b/server/website/api_more.html
@@ -19,7 +19,8 @@
     
- + API Documentation + Send

Simple Cloud Notifier

@@ -30,7 +31,7 @@ With this API you can send push notifications to your phone.

- To recieve them you will need to install the SimpleCloudNotifier app from the play store. + To receive them you will need to install the SimpleCloudNotifier app from the play store. When you open the app you can click on the account tab to see you unique user_id and user_key. These two values are used to identify and authenticate your device so that send messages can be routed to your phone.

@@ -45,7 +46,7 @@

Quota

- By default you can send up to 100 messages per day per device. + By default you can send up to 50 messages per day per device. If you need more you can upgrade your account in the app to get 1000 messages per day, this has the additional benefit of removing ads and supporting the development of the app (and making sure I can pay the server costs).

@@ -54,10 +55,10 @@

To send a new notification you send a POST request to the URL https://scn.blackforestbytes.com/. - All Parameters can either directly be submitted as URL parameters or they can be put into the POST body. + All Parameters can either directly be submitted as URL parameters or they can be put into the POST body (either multipart/form-data or JSON).

- You need to supply a valid user_id - user_key pair and a title for your message, all other parameter are optional. + You need to supply a valid [user_id, user_key] pair and a title for your message, all other parameter are optional.

@@ -81,7 +82,7 @@ "quota_max":100 }

- If the operation is not successful the API will respond with an 4xx HTTP statuscode. + If the operation is not successful the API will respond with a 4xx or 500 HTTP statuscode.

@@ -107,10 +108,6 @@ - - - - @@ -122,10 +119,10 @@ The success field is always there and in the error state you the message field to get a descritpion of the problem.

{
-    "success":false,
-    "error":2101,
-    "errhighlight":-1,
-    "message":"Daily quota reached (100)"
+    "success": false,
+    "error": 2101,
+    "errhighlight": -1,
+    "message": "Daily quota reached (100)"
 }
@@ -133,7 +130,7 @@

Every message must have a title set. - But you also (optionally) add more content, while the title has a max length of 120 characters, the conntent can be up to 10.000 characters. + But you also (optionally) add more content, while the title has a max length of 120 characters, the content can be up to 10.000 characters. You can see the whole message with title and content in the app or when clicking on the notification.

@@ -165,6 +162,21 @@ https://scn.blackforestbytes.com/s

+

Channels

+
+

+ By default all messages are sent to the user default channel (typically main) + You can specify a different channel with the channel parameter, if the channel does not already exist it will be created. + Channel names are case-insensitive and can only contain letters, numbers, underscores and minuses ( /[[:alnum:]\-_]+/ ) +

+
curl                                          \
+    --data "user_id={userid}"                 \
+    --data "user_key={userkey}"               \
+    --data "title={message_title}"            \
+    --data "channel={my_channel}"             \
+    https://scn.blackforestbytes.com/s
+
+

Message Uniqueness

@@ -172,7 +184,7 @@

To ensure that a message is only send once you can generate a unique id for your message (I would recommend a simple uuidgen). - If you send a message with an UUID that was already used in the near past the API still returns OK, but no new message is sent. + If you send a message with a UUID that was already used in the near past the API still returns OK, but no new message is sent.

The message_id is optional - but if you want to use it you need to supply it via the msg_id parameter. @@ -208,90 +220,9 @@

Depending on your use case it can be useful to create a bash script that handles things like resending messages if you have connection problems or waiting if there is no quota left.
- Here is an example how such a scrippt could look like, you can put it into /usr/local/sbin and call it with scn_send "title" "content" -

-
#!/usr/bin/env bash
-
-#
-# Call with   `scn_send title`
-#        or   `scn_send title content`
-#        or   `scn_send title content priority`
-#
-#
-
-if [ "$#" -lt 1 ]; then
-    echo "no title supplied via parameter"
-    exit 1
-fi
-
-################################################################################
-# INSERT YOUR DATA HERE                                                        #
-################################################################################
-user_id=999
-user_key="????????????????????????????????????????????????????????????????"
-################################################################################
-
-title=$1
-content=""
-sendtime=$(date +%s)
-
-if [ "$#" -gt 1 ]; then
-    content=$2
-fi
-
-priority=1
-
-if [ "$#" -gt 2 ]; then
-    priority=$3
-fi
-
-usr_msg_id=$(uuidgen)
-
-while true ; do
-
-    curlresp=$(curl -s -o /dev/null -w "%{http_code}" \
-                    -d "user_id=$user_id" -d "user_key=$user_key" -d "title=$title" -d "timestamp=$sendtime" \
-                    -d "content=$content" -d "priority=$priority" -d "msg_id=$usr_msg_id" \
-                    https://scn.blackforestbytes.com/)
-
-    if [ "$curlresp" == 200 ] ; then
-        echo "Successfully send"
-        exit 0
-    fi
-
-    if [ "$curlresp" == 400 ] ; then
-        echo "Bad request - something went wrong"
-        exit 1
-    fi
-
-    if [ "$curlresp" == 401 ] ; then
-        echo "Unauthorized - wrong userid/userkey"
-        exit 1
-    fi
-
-    if [ "$curlresp" == 403 ] ; then
-        echo "Quota exceeded - wait one hour before re-try"
-        sleep 3600
-    fi
-
-    if [ "$curlresp" == 412 ] ; then
-        echo "Precondition Failed - No device linked"
-        exit 1
-    fi
-
-    if [ "$curlresp" == 500 ] ; then
-        echo "Internal server error - waiting for better times"
-        sleep 60
-    fi
-
-    # if none of the above matched we probably hav no network ...
-    echo "Send failed (response code $curlresp) ... try again in 5s"
-    sleep 5
-done
-
-

- Be aware that the server only saves send messages for a short amount of time. Because of that you can only use this to prevent duplicates in a short time-frame, older messages with the same ID are probably already deleted and the message will be send again. + Here is an example how such a scrippt could look like, you can put it into /usr/local/sbin and call it with scn_send "title" "content" (or with more parameters, see the script itself)

+
{{template|scn_send.html}}
diff --git a/server/website/css/style.css b/server/website/css/style.css index 3ac9347..0e04e7e 100644 --- a/server/website/css/style.css +++ b/server/website/css/style.css @@ -22,19 +22,18 @@ body #mainpnl { box-shadow: 0 0 32px #DDD; - //animation:blink-shadow ease-in-out 4s infinite; width: 87%; min-width: 300px; max-width: 900px; position: relative; - min-height: 570px; + min-height: 525px; background: var(--form-back-color); color: var(--form-fore-color); border: .0625rem solid var(--form-border-color); border-radius: var(--universal-border-radius); margin: 32px .5rem; - padding: calc(2 * var(--universal-padding)) var(--universal-padding); + padding: calc(2 * var(--universal-padding)) var(--universal-padding) var(--universal-padding); } .red-code @@ -126,7 +125,7 @@ body text-align: center; } -#tl_link +#tl_link1 { position: absolute; top: 0; @@ -137,6 +136,37 @@ body padding: 4px 4px 0 4px; } +#tl_link2 +{ + position: absolute; + top: 0; + left: 48px; + margin: -1px 0 0 -1px; + border-top-right-radius: 0; + border-bottom-left-radius: 0; + padding: 4px 4px 0 4px; +} + +#tl_linkDocs +{ + position: absolute; + top: 0; + left: 0; + margin: -1px 0 0 -1px; + border-top-right-radius: 0; + border-bottom-left-radius: 0; + padding: 4px; + display: flex; +} + +.tl_btntxt +{ + margin-left: 0.66rem; + margin-right: 0.33rem; + margin-top: -1px; + align-self: center; +} + .icn-google-play { display: inline-block; width: 32px; @@ -145,6 +175,22 @@ body background-size: 100%; } +.icn-app-store { + display: inline-block; + width: 32px; + height: 32px; + background: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwMCAxMDAwIj4KCTxwYXRoIGZpbGw9IiM0NDQiIGQ9Ik05MjEuMiw3MjljLTIzLDUxLTM0LjEsNzMuOC02My43LDExOC45Yy00MS4zLDYyLjktOTkuNiwxNDEuMy0xNzEuOSwxNDJjLTY0LjIsMC42LTgwLjYtNDEuOC0xNjcuOC00MS4zYy04Ny4xLDAuNS0xMDUuMyw0Mi0xNjkuNSw0MS40Yy03Mi4yLTAuNy0xMjcuNS03MS41LTE2OC44LTEzNC40QzYzLjksNjc5LjYsNTEuOCw0NzMsMTIzLjEsMzYzLjJjNTAuNy03OCwxMzAuNy0xMjMuNywyMDUuOS0xMjMuN2M3Ni42LDAsMTI0LjcsNDIsMTg4LDQyYzYxLjQsMCw5OC44LTQyLDE4Ny4zLTQyYzY2LjksMCwxMzcuOCwzNi40LDE4OC4zLDk5LjRDNzI3LjEsNDI5LjUsNzU0LDY2NS44LDkyMS4yLDcyOXogTTYzNy4xLDE2OS4xYzMyLjItNDEuMyw1Ni42LTk5LjYsNDcuNy0xNTkuMWMtNTIuNSwzLjYtMTE0LDM3LTE0OS45LDgwLjZjLTMyLjYsMzkuNS01OS40LDk4LjItNDksMTU1LjJDNTQzLjQsMjQ3LjYsNjAyLjcsMjEzLjMsNjM3LjEsMTY5LjF6Ii8+Cjwvc3ZnPg==') 50% 50% no-repeat; + background-size: 100%; +} + +.icn-openapi { + display: inline-block; + width: 32px; + height: 32px; + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cgo8c3ZnCiAgIHdpZHRoPSI2NzQuMzEzMTEiCiAgIGhlaWdodD0iNjcwLjY2MjA1IgogICB2aWV3Qm94PSIwIDAgMTc4LjQxMjAxIDE3Ny40NDYiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzUiCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnMKICAgICBpZD0iZGVmczIiIC8+CiAgPGcKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBkPSJNIDYxLjIxNDgxOCw5MS44MzkxODQgSCAxOS4zMzM5MjMgYyAwLjAwNzksMC4yMDYzOCAwLjAxMzIzLDAuNDEwMTEgMC4wMjM4MSwwLjYxMzgzIDAuMDE1ODgsMC4zOTQyMyAwLjA0NDk4LDAuNzg1ODIgMC4wNjg3OSwxLjE4MDA1IDAuMDEwNTgsMC4xNjkzMyAwLjAxODUyLDAuMzM4NjYgMC4wMzE3NSwwLjUwNTM1IDAuMDMxNzUsMC40NTUwOCAwLjA3MTQ0LDAuOTEwMTcgMC4xMTM3NzEsMS4zNjI2IDAuMDEwNTgsMC4xMDMxOSAwLjAxODUyLDAuMjA2MzggMC4wMjkxLDAuMzA0MjggMC4wNDc2MywwLjUwNTM1IDAuMTAzMTg3LDEuMDEwNyAwLjE2MTM5NiwxLjUxMDc3IDAuMDAyNiwwLjA0NzYgMC4wMDc5LDAuMDk3OSAwLjAxNTg3LDAuMTQ1NTIgMC4wNjYxNSwwLjUzOTc1IDAuMTM0OTM3LDEuMDgyMTQgMC4yMTQzMTIsMS42MTkyNSAwLDAuMDA4IDAsMC4wMTU5IDAuMDAyNiwwLjAyMTIgYSA3MC4zOTIzOTYsNzAuMzkyMzk2IDAgMCAwIDIuMTY0MjkyLDkuODk1NDA2IGMgMC4wMDUzLDAuMDEzMiAwLjAxMDU4LDAuMDI2NSAwLjAxMzIzLDAuMDM5NyAwLjE1MDgxMiwwLjUxMzI5IDAuMzA5NTYyLDEuMDIxMjkgMC40NzA5NTgsMS41MjY2NCAwLjAxMDU4LDAuMDI5MSAwLjAyMTE3LDAuMDYwOSAwLjAyOTEsMC4wOSAwLjAwNzksMC4wMjkxIDAuMDE4NTIsMC4wNTgyIDAuMDI5MSwwLjA5IDAuMTQ4MTY3LDAuNDU3NzMgMC4zMDQyNzEsMC45MTI4MSAwLjQ2MDM3NSwxLjM3MDU0IDAuMDM5NjksMC4xMTM3NyAwLjA3OTM4LDAuMjMwMTkgMC4xMjE3MDgsMC4zNDY2IDAuMTQwMjMsMC4zOTQyMyAwLjI4MzEwNSwwLjc4ODQ2IDAuNDMxMjcxLDEuMTg1MzQgMC4wNjg3OSwwLjE3NzI3IDAuMTM0OTM4LDAuMzU0NTQgMC4yMDEwODQsMC41MzQ0NiAwLjEyOTY0NSwwLjMyODA4IDAuMjU0LDAuNjUzNTIgMC4zODM2NDUsMC45Nzg5NSAwLjA5NzksMC4yNDYwNyAwLjE5ODQzOCwwLjQ5MjEzIDAuMzAxNjI1LDAuNzM1NTUgMC4xMDU4MzQsMC4yNTY2NCAwLjIxMTY2NywwLjUwOCAwLjMyMDE0NiwwLjc2NDY0IDAuMTMyMjkyLDAuMzEyMjEgMC4yNzI1MjEsMC42MjE3NyAwLjQxMDEwNCwwLjkzMzk4IDAuMDgyMDIsMC4xODUyMSAwLjE2NjY4OCwwLjM2Nzc3IDAuMjQ4NzA5LDAuNTUyOTggMC4xNjkzMzMsMC4zNzMwNiAwLjM0Mzk1OCwwLjc0NjEyIDAuNTIxMjI5LDEuMTE2NTQgMC4wNTgyMSwwLjExOTA2IDAuMTEzNzcxLDAuMjM4MTMgMC4xNzE5NzksMC4zNTQ1NCAwLjIwNjM3NSwwLjQyODYzIDAuNDE1Mzk2LDAuODQ5MzIgMC42MjcwNjMsMS4yNyAwLjAzNzA0LDAuMDYzNSAwLjA2NjE1LDAuMTI5NjUgMC4xMDA1NDEsMC4xOTMxNSAwLjIzODEyNSwwLjQ2NTY3IDAuNDgxNTQyLDAuOTI4NjkgMC43MzI4OTYsMS4zODkwNiAwLjAxMDU4LDAuMDIxMiAwLjAyMzgxLDAuMDM5NyAwLjAzMTc1LDAuMDYzNSAwLjA1MjkyLDAuMDk1MiAwLjExMTEyNSwwLjE4Nzg2IDAuMTY0MDQyLDAuMjgzMTEgbCAzNS43NjkwMjEsLTIxLjU0NTAzIDAuMTMyMjkxLC0wLjA3OTQgYyAtMS40MTAyMjksLTIuOTczOTA2IC0yLjI4ODY0NiwtNi4xMzgzMjYgLTIuNjE2NzI5LC05LjM1MzAxNiB6IgogICAgICAgZmlsbD0iIzkzZDUwMCIKICAgICAgIGlkPSJwYXRoOSIKICAgICAgIHN0eWxlPSJzdHJva2Utd2lkdGg6MC4yNjQ1ODMiIC8+CiAgICA8cGF0aAogICAgICAgZD0ibSA3MS41MDk3NTYsMTExLjA2NjQ1IC0wLjEwMDU0MiwwLjEwMDU0IC0yOS41MDEwNDIsMjkuNDk4NCBjIDAuMTUwODEzLDAuMTQwMjMgMC4zMDE2MjUsMC4yODU3NSAwLjQ1NzcyOSwwLjQyMDY4IDAuMjc1MTY3LDAuMjUxMzYgMC41NTU2MjUsMC40OTc0MiAwLjgzMzQzOCwwLjc0MzQ4IDAuMTQ1NTIxLDAuMTI3IDAuMjg1NzUsMC4yNTQgMC40MzEyNzEsMC4zODEgMC4zMzMzNzUsMC4yODg0IDAuNjcyMDQxLDAuNTY4ODYgMS4wMDgwNjIsMC44NTE5NiAwLjA5MjYsMC4wNzY3IDAuMTc5OTE3LDAuMTUwODEgMC4yNzI1MjEsMC4yMjc1NCBhIDY3LjYyMTE1LDY3LjYyMTE1IDAgMCAwIDEuMjk5MTA0LDEuMDQyNDYgYyAwLjQxODA0MiwwLjMzMDczIDAuODM4NzI5LDAuNjUwODggMS4yNjczNTQsMC45NzM2NyAwLjAxODUyLDAuMDEwNiAwLjAzMTc1LDAuMDIxMiAwLjA0NzYzLDAuMDM0NCBhIDcwLjUwNjE2Nyw3MC41MDYxNjcgMCAwIDAgNS41MTkyMDksMy43MzA2MyBjIDAuMDYzNSwwLjA0MjMgMC4xMjk2NDYsMC4wNzk0IDAuMTk1NzkxLDAuMTIxNzEgMC4zNjI0OCwwLjIxNDMxIDAuNzIyMzEzLDAuNDI4NjIgMS4wODc0MzgsMC42NDAyOSBsIDAuNjY2NzUsMC4zODEgYyAwLjIxMTY2NywwLjEyMTcxIDAuNDIwNjg3LDAuMjM4MTIgMC42MzIzNTQsMC4zNTQ1NCAwLjM4MzY0NiwwLjIwOTAyIDAuNzY3MjkyLDAuNDE4MDQgMS4xNTM1ODQsMC42MjE3NyAwLjA1MjkyLDAuMDI5MSAwLjEwNTgzMywwLjA1ODIgMC4xNjEzOTUsMC4wODQ3IDEuMDcxNTYzLDAuNTYwOTIgMi4xNjE2NDYsMS4wODIxNSAzLjI2MjMxMywxLjU4NDg1IGwgMS4wNDc3NSwtMi41NDI2NCAxNC44Mzc4MzMsLTM2LjAyODMxIDAuMDUyOTIsLTAuMTMyMyBjIC0xLjYxOTI1LC0wLjg2NzgzIC0zLjE3MjM1NCwtMS44OTQ0MSAtNC42MzI4NTQsLTMuMDkwMzMgeiIKICAgICAgIGZpbGw9IiM0ZDVhMzEiCiAgICAgICBpZD0icGF0aDExIgogICAgICAgc3R5bGU9InN0cm9rZS13aWR0aDowLjI2NDU4MyIgLz4KICAgIDxwYXRoCiAgICAgICBkPSJtIDY4LjQzNzk0MywxMDguMTI2OTMgYSAyNS4yNjU1OTIsMjUuMjY1NTkyIDAgMCAxIC0wLjkyMDc1LC0xLjA4NDc5IGMgLTAuMjY0NTgzLC0wLjMyNTQ0IC0wLjUxODU4MywtMC42NTM1MiAtMC43NjQ2NDYsLTAuOTg2OSAtMC4yODMxMDQsLTAuMzgxIC0wLjU1ODI3MSwtMC43NjcyOSAtMC44MjAyMDgsLTEuMTU4ODcgLTAuMjYxOTM4LC0wLjM5NDIzIC0wLjUyMTIyOSwtMC43OTExMSAtMC43NjQ2NDYsLTEuMTkzMjcgbCAtMzUuODE0LDIxLjU3MTQ3IGMgMC41NTAzMzQsMC45MTU0NiAxLjEyMTgzNCwxLjgwNDQ2IDEuNzA5MjA5LDIuNjgyODggMC4wMTg1MiwwLjAzMTggMC4wMzcwNCwwLjA2MzUgMC4wNTgyMSwwLjA5MjYgMC4wMDUzLDAuMDEzMiAwLjAxMzIzLDAuMDIxMiAwLjAyMTE3LDAuMDM0NCAwLjAxODUyLDAuMDI2NSAwLjAzNzA0LDAuMDU4MiAwLjA1NTU2LDAuMDg3MyAwLjAwMjYsMC4wMDMgMC4wMDUzLDAuMDA1IDAuMDA1MywwLjAxMDYgMC4wNDc2MywwLjA2ODggMC4wOTUyNSwwLjEzNzU4IDAuMTQyODc1LDAuMjA5MDIgMC4wMDI2LDAuMDAzIDAuMDA1MywwLjAwOCAwLjAxMDU4LDAuMDEzMiAwLjAwNTMsMC4wMDUgMC4wMDI2LDAuMDAzIDAuMDAyNiwwLjAwNSAwLjYyMTc3MSwwLjkxODEgMS4yNjIwNjIsMS44MjI5OCAxLjkyMDg3NSwyLjcwNjY4IDAuMDEzMjMsMC4wMTg1IDAuMDI5MSwwLjAzNyAwLjA0MjMzLDAuMDU1NiAwLjAxNTg3LDAuMDE4NSAwLjAyOTEsMC4wMzcgMC4wNDIzMywwLjA1NTYgMC4yODgzOTUsMC4zNzgzNSAwLjU3NDE0NSwwLjc1NjcxIDAuODcwNDc5LDEuMTMyNDIgMC4wMzk2OSwwLjA1MDMgMC4wNzY3MywwLjA5NzkgMC4xMTkwNjIsMC4xNDU1MiAwLjMwMTYyNSwwLjM4ODkzIDAuNjExMTg4LDAuNzcyNTggMC45MjYwNDIsMS4xNTA5MyAwLjA3NDA4LDAuMDkgMC4xNTA4MTIsMC4xNzk5MiAwLjIyNDg5NiwwLjI3MjUzIDAuMjgzMTA0LDAuMzM4NjYgMC41NjYyMDgsMC42NzQ2OCAwLjg1NDYwNCwxLjAwODA2IDAuMTI0MzU0LDAuMTQyODcgMC4yNTEzNTQsMC4yODgzOSAwLjM3ODM1NCwwLjQyODYyIDAuMjQ2MDYzLDAuMjgwNDYgMC40ODk0NzksMC41NjA5MiAwLjc0MDgzMywwLjgzNjA5IDAuMTM3NTg0LDAuMTUwODEgMC4yODA0NTksMC4zMDQyNyAwLjQyMDY4OCwwLjQ1NTA4IDAuMDYzNSwwLjA3MTQgMC4xMjcsMC4xMzc1OCAwLjE5MzE0NiwwLjIwOTAyIDAuMTgyNTYyLDAuMTk4NDQgMC4zNjUxMjUsMC4zOTY4OCAwLjU1MDMzMywwLjU5MjY3IDAuMDc0MDgsMC4wNzY3IDAuMTUzNDU4LDAuMTUzNDYgMC4yMjc1NDIsMC4yMzI4MyAwLjMyNTQzNywwLjMzODY3IDAuNjU2MTY2LDAuNjc5OTggMC45ODk1NDEsMS4wMTMzNSBsIDI5LjU0NjAyMSwtMjkuNTQwNzIgYyAtMC4zMzg2NjcsLTAuMzM4NjcgLTAuNjU2MTY2LC0wLjY4NzkyIC0wLjk2ODM3NSwtMS4wMzcxNyB6IgogICAgICAgZmlsbD0iIzZiYTQzYSIKICAgICAgIGlkPSJwYXRoMTMiCiAgICAgICBzdHlsZT0ic3Ryb2tlLXdpZHRoOjAuMjY0NTgzIiAvPgogICAgPHBhdGgKICAgICAgIGQ9Im0gMTA3LjU5ODkyLDExMS4wNTA1NyBjIC0wLjMyNTQzLDAuMjYxOTQgLTAuNjUzNTIsMC41MTg1OSAtMC45ODY4OSwwLjc2NDY1IGwgMC4wNzE0LDAuMTI0MzUgMjEuNTQyMzcsMzUuNzUzMTUgYyAwLjk5NzQ4LC0wLjY1MDg3IDEuOTg0MzcsLTEuMzIyOTIgMi45NDc0NiwtMi4wMjkzNSAyLjA4NDkxLC0xLjUzNzIzIDQuMTAxMDQsLTMuMjA0MTEgNi4wNDU3MywtNC45OTc5OCBsIC0yOS41MDM2OSwtMjkuNDk4NCB6IgogICAgICAgZmlsbD0iIzRkNWEzMSIKICAgICAgIGlkPSJwYXRoMTUiCiAgICAgICBzdHlsZT0ic3Ryb2tlLXdpZHRoOjAuMjY0NTgzIiAvPgogICAgPHBhdGgKICAgICAgIGQ9Im0gMTI1LjA5NTgyLDE0Ny45ODY0MSAtMC43MzAyNSwtMS4yMTQ0NCAtMjAuMTA4MzMsLTMzLjM3MTkgYyAtMC40MDc0NiwwLjI0MzQyIC0wLjgyMjg2LDAuNDY1NjcgLTEuMjM4MjUsMC42ODc5MiAtMC40MjA2OSwwLjIyNzU0IC0wLjg0MTM4LDAuNDM2NTYgLTEuMjcsMC42NDAyOSAtMy44NjI5MjEsMS44MzA5MiAtOC4wNDMzMzksMi43Njc1NCAtMTIuMjMxNjkzLDIuNzY3NTQgLTIuNzQzNzI5LDAgLTUuNDg0ODEyLC0wLjM5Njg3IC04LjEzNTkzNywtMS4xODc5NyAtMC40NTI0MzgsLTAuMTM0OTQgLTAuODk0MjkyLC0wLjMxMjIxIC0xLjMzODc5MiwtMC40NzA5NiAtMC40NDcxNDYsLTAuMTU2MTEgLTAuODk5NTgzLC0wLjI5MzY5IC0xLjMzODc5MiwtMC40NzM2MSBsIC0xNC44MjcyNSwzNi4wMDQ1IC0wLjU3OTQzNywxLjQxMDIzIC0wLjQ5NzQxNywxLjIwOTE1IC0wLjAwMjYsMC4wMDUgYyAwLjAzNzA0LDAuMDE1OSAwLjA3NjczLDAuMDI5MSAwLjExMzc3MSwwLjA0NSAwLjA0MjMzLDAuMDE1OSAwLjA4MjAyLDAuMDI5MSAwLjEyMTcwOSwwLjA0NzYgaCAwLjAwNTMgYyAwLjAxMDU4LDAuMDA1IDAuMDIxMTcsMC4wMDggMC4wMjkxLDAuMDEwNiAwLjM0OTI1LDAuMTQ1NTIgMC43MDExNDYsMC4yNjcyMyAxLjA1MzA0MiwwLjQwNDgxIDAuNDQxODU0LDAuMTcxOTggMC44ODEwNjMsMC4zNDY2IDEuMzIyOTE3LDAuNTA4IDAuMjI3NTQxLDAuMDgyIDAuNDQ5NzkxLDAuMTc3MjcgMC42NzQ2ODcsMC4yNTY2NSBoIDAuMDAyNiBhIDcwLjc1NDg3NSw3MC43NTQ4NzUgMCAwIDAgMTQuMzM3NzcxLDMuNDMxNjQgYyAwLjE5MzE0NiwwLjAyMzggMC4zODEsMC4wNTAzIDAuNTc0MTQ2LDAuMDc2NyAwLjE5ODQzNywwLjAyMzggMC4zOTk1MjEsMC4wNDIzIDAuNjAwNjA0LDAuMDY2MSAwLjM3NTcwOCwwLjA0MjMgMC43NTE0MTcsMC4wODQ3IDEuMTI5NzcxLDAuMTIxNyBsIDAuMjgwNDU4LDAuMDIzOCBjIDAuNDYzMDIxLDAuMDQ1IDAuOTI2MDQyLDAuMDg0NyAxLjM4NjQxNywwLjExNjQxIDAuMTU4NzUsMC4wMTMyIDAuMzE3NSwwLjAxODUgMC40NzM2MDQsMC4wMzE3IDAuNDA0ODEyLDAuMDI2NSAwLjgwOTYyNSwwLjA1MDMgMS4yMTQ0MzcsMC4wNjg4IDAuMjQ4NzA5LDAuMDE1OSAwLjQ5NzQxNywwLjAyMTIgMC43NDYxMjUsMC4wMzE3IDAuMzIyNzkyLDAuMDEwNiAwLjY0MjkzOCwwLjAyMzggMC45NjU3MywwLjAzNDQgMC40NjMwMiwwLjAxMDYgMC45MzEzMzMsMC4wMTMyIDEuMzk3LDAuMDE1OSAwLjExMTEyNSwwIDAuMjE5NjA0LDAuMDAzIDAuMzMwNzI5LDAuMDA1IDMuOTAyNjA0LDAgNy44MDUyMDksLTAuMzI1NDQgMTEuNjY1NDgsLTAuOTczNjcgMC4wNjM1LC0wLjAxMDYgMC4xMjk2NSwtMC4wMjEyIDAuMTk1NzksLTAuMDI5MSAwLjQwNzQ2LC0wLjA3MTQgMC44MTQ5MiwtMC4xNDgxNyAxLjIyMjM4LC0wLjIyMjI1IDAuMjMyODMsLTAuMDQ1IDAuNDY4MzEsLTAuMDkgMC42OTU4NSwtMC4xMzQ5NCAwLjIzODEzLC0wLjA1MDMgMC40Nzg5LC0wLjA5NzkgMC43MTcwMiwtMC4xNTA4MSAwLjM5Njg4LC0wLjA4MiAwLjc5Mzc1LC0wLjE2OTM0IDEuMTkzMjcsLTAuMjYxOTQgMC4wNzE0LC0wLjAxNTkgMC4xNDI4OCwtMC4wMjkxIDAuMjE0MzEsLTAuMDUwMyA1Ljg2ODQ2LC0xLjM2NTI1IDExLjUzNTg0LC0zLjQ4MTkyIDE2Ljg1OTI2LC02LjI4Mzg2IHYgLTAuMDAzIGMgMC4zNDkyNSwtMC4xODI1NiAwLjY5MDU2LC0wLjM5MTU4IDEuMDM3MTcsLTAuNTgyMDggMC40MTI3NSwtMC4yMjQ5IDAuODIyODUsLTAuNDYwMzggMS4yMzU2LC0wLjY5NTg2IDAuMjgzMSwtMC4xNjEzOSAwLjU3MTUsLTAuMzEyMjEgMC44NTQ2LC0wLjQ4MTU0IDAuMDAzLC0wLjAwMyAwLjAwNSwtMC4wMDUgMC4wMDgsLTAuMDA1IGggMC4wMDMgYyAwLjAwNSwtMC4wMDMgMC4wMDgsLTAuMDA1IDAuMDEzMiwtMC4wMDggbCAwLjAwNSwtMC4wMDUgLTAuMDUyOSwtMC4wODczIDAuMDUyOSwwLjA4NzMgYyAwLjAwMywwIDAuMDA1LC0wLjAwMyAwLjAwOCwtMC4wMDMgMC4wMDUsLTAuMDAzIDAuMDA4LC0wLjAwNSAwLjAxMzIsLTAuMDA4IDAuMDE1OSwtMC4wMTA2IDAuMDMxOCwtMC4wMTg1IDAuMDQ3NiwtMC4wMjkxIDAuMDksLTAuMDU1NiAwLjE4MjU2LC0wLjEwODQ4IDAuMjc1MTcsLTAuMTY0MDQgeiBNIDcxLjUyNTYzMSw2Ni45NjU3MDQgYyAwLjMyNTQzNywtMC4yNjQ1OCAwLjY1NjE2NiwtMC41MTg1OCAwLjk4Njg5NSwtMC43NjcyOSBsIC0wLjA3MTQ0LC0wLjEyMTcxIC0yMS41NDIzNzUsLTM1Ljc1NTc5MyBjIC0xLjAwMDEyNSwwLjY1MzUyMSAtMS45ODcwMjEsMS4zMjgyMDkgLTIuOTUyNzUsMi4wMzQ2NDYgLTIuMDgyMjcxLDEuNTM0NTgzIC00LjEwMTA0MiwzLjE5ODgxMyAtNi4wNDA0MzgsNC45OTAwNDMgbCAyOS41MDM2ODgsMjkuNTAxMDQ0IHoiCiAgICAgICBmaWxsPSIjOTNkNTAwIgogICAgICAgaWQ9InBhdGgxNyIKICAgICAgIHN0eWxlPSJzdHJva2Utd2lkdGg6MC4yNjQ1ODMiIC8+CiAgICA8cGF0aAogICAgICAgZD0ibSAzOS44NjAyOTcsMzkuMzExNDUgYyAtMC4zMzYwMjEsMC4zMzYwMiAtMC42NTM1MjEsMC42ODI2MyAtMC45ODE2MDQsMS4wMjEyOSAtMC4zMjU0MzcsMC4zNDEzMiAtMC42NTg4MTIsMC42Nzk5OCAtMC45Nzg5NTgsMS4wMjM5NCBhIDcwLjQyMDQ0Miw3MC40MjA0NDIgMCAwIDAgLTUuOTY5LDcuMzk3NzUgYyAtMC4wODczMSwwLjExOTA2IC0wLjE3MTk3OSwwLjI0NjA2IC0wLjI1OTI5MiwwLjM2Nzc3IC0wLjIwMTA4MywwLjI5NjM0IC0wLjQwMjE2NywwLjU5MDAyIC0wLjU5Nzk1OCwwLjg4NjM2IC0wLjIwNjM3NSwwLjMwNjkxIC0wLjQxMDEwNCwwLjYxOTEyIC0wLjYwODU0MiwwLjkyODY4IC0wLjA3NDA4LDAuMTEzNzcgLTAuMTQ4MTY2LDAuMjMwMTkgLTAuMjE5NjA0LDAuMzQzOTYgLTYuNzg2NTYyLDEwLjY0NDE5NCAtMTAuNDIxOTM3LDIyLjcxNzEzNCAtMTAuOTExNDE2LDM0Ljg5MDYxNCAtMC4wMTg1MiwwLjQ3MzYgLTAuMDI5MTEsMC45NDcyIC0wLjAzOTY5LDEuNDE4MTYgLTAuMDA3OSwwLjQ3MDk2IC0wLjAyMzgxLDAuOTQ0NTcgLTAuMDIzODEsMS40MTU1MiBoIDQxLjc4NTY0NSBjIDAsLTAuNDcwOTUgMC4wNDIzMywtMC45NDE5MSAwLjA2ODc5LC0xLjQxNTUyIDAuMDIzODEsLTAuNDczNiAwLjAyOTEsLTAuOTQ3MjEgMC4wNzY3MywtMS40MTgxNiAwLjUzOTc1LC01LjQzOTg0IDIuNjQwNTQyLC0xMC43NTI2NyA2LjMwMjM3NSwtMTUuMjEzNTUgMC4yOTYzMzMsLTAuMzYyNDcgMC42MzIzNTQsLTAuNzAzNzkgMC45NTI1LC0xLjA1MzA0IDAuMzE0ODU0LC0wLjM1MTg5IDAuNjExMTg3LC0wLjcxNDM3IDAuOTQ5ODU0LC0xLjA1MzA0IHogTSAxMjYuMDc0NzgsMjguOTM5Nzg2IGMgLTAuMDU1NiwtMC4wMzQ0IC0wLjExNjQyLC0wLjA2ODc5IC0wLjE3MTk4LC0wLjEwMzE4NyAtMC4zNjc3NywtMC4yMjQ4OTYgLTAuNzM4MTksLTAuNDQxODU0IC0xLjExMTI1LC0wLjY1ODgxMyAtMC4yMTQzMSwtMC4xMjQzNTQgLTAuNDMxMjcsLTAuMjQ2MDYyIC0wLjY0NTU5LC0wLjM2Nzc3MSAtMC4yMTk2LC0wLjEyMTcwOCAtMC40MzM5MSwtMC4yNDM0MTYgLTAuNjUzNTIsLTAuMzYyNDc5IGEgMjM4LjQ5NTQyLDIzOC40OTU0MiAwIDAgMCAtMS4xMzUwNiwtMC42MTM4MzMgYyAtMC4wNjA5LC0wLjAyOTEgLTAuMTE5MDYsLTAuMDYwODUgLTAuMTc5OTIsLTAuMDkyNiAtMi40NTAwNCwtMS4yODA1ODQgLTQuOTY2MjIsLTIuNDA3NzA5IC03LjUzNTMzLC0zLjM4NjY2NyAtMC4wNjYyLC0wLjAyNjQ2IC0wLjEzMjI5LC0wLjA1MjkyIC0wLjE5ODQ0LC0wLjA3NjczIC0wLjU2MDkxLC0wLjIwOTAyMSAtMS4xMjE4MywtMC40MTgwNDIgLTEuNjg4MDQsLTAuNjEzODMzIEEgNzAuNjY1OTc1LDcwLjY2NTk3NSAwIDAgMCA5OC42NzcxNjksMTkuMzIyMTgyIGwgLTAuNTg3MzcsLTAuMDc5MzggYyAtMC4xOTg0NCwtMC4wMjM4MSAtMC4zOTQyMywtMC4wNDIzMyAtMC41OTAwMiwtMC4wNjM1IC0wLjM3ODM2LC0wLjA0NDk4IC0wLjc1NDA3LC0wLjA4NzMxIC0xLjEzNTA3LC0wLjEyNDM1NCAtMC4xMDMxOCwtMC4wMTA1OCAtMC4yMDkwMiwtMC4wMTU4OCAtMC4zMTc0OTUsLTAuMDI5MSAtMC40NDcxNDYsLTAuMDM5NjkgLTAuODk5NTgzLC0wLjA3OTM3IC0xLjM0OTM3NSwtMC4xMTM3NzEgbCAtMC41MjY1MjEsLTAuMDMxNzUgYyAtMC4zODM2NDYsLTAuMDIzODEgLTAuNzc1MjI5LC0wLjA1MjkyIC0xLjE1ODg3NSwtMC4wNjg3OSAtMC4yMDYzNzUsLTAuMDEwNTggLTAuNDEyNzUsLTAuMDE4NTIgLTAuNjE2NDc5LC0wLjAyMzgxIHYgNDEuODgwODk3IGMgMi4xNDg0MTcsMC4yMjIyNSA0LjI3ODMxNSwwLjY3OTk4IDYuMzQ0NzA1LDEuMzgxMTMgTCAxMjkuNTY3MjgsMzEuMjI4NDMyIGMgLTEuMTQ1NjUsLTAuNzkzNzUgLTIuMzAxODgsLTEuNTY2MzMzIC0zLjQ5MjUsLTIuMjg4NjQ2IHoiCiAgICAgICBmaWxsPSIjNGQ1YTMxIgogICAgICAgaWQ9InBhdGgxOSIKICAgICAgIHN0eWxlPSJzdHJva2Utd2lkdGg6MC4yNjQ1ODMiIC8+CiAgICA8cGF0aAogICAgICAgZD0ibSA4OC4xNDY3NTYsMTguNzUzMzI4IGMgLTAuNDczNjA1LDAuMDEwNTggLTAuOTQ3MjA5LDAuMDEwNTggLTEuNDE4MTY3LDAuMDI5MSAtMi45NTUzOTYsMC4xMTkwNjMgLTUuOTA1NSwwLjQyMDY4OCAtOC44MzE3OTIsMC45MTI4MTMgLTAuMDYzNSwwLjAxMDU4IC0wLjEyNywwLjAyMTE3IC0wLjE5MzE0NiwwLjAzMTc1IC0wLjQxMDEwNCwwLjA3MTQ0IC0wLjgxNzU2MiwwLjE0NTUyIC0xLjIyNTAyLDAuMjIyMjUgLTAuMjMwMTg4LDAuMDQ0OTggLTAuNDYzMDIxLDAuMDg3MzEgLTAuNjk1ODU1LDAuMTMyMjkxIC0wLjI0MDc3LDAuMDQ3NjMgLTAuNDc4ODk1LDAuMDk3OSAtMC43MTk2NjYsMC4xNTA4MTMgLTAuMzk2ODc1LDAuMDgyMDIgLTAuNzkzNzUsMC4xNjkzMzMgLTEuMTg3OTc5LDAuMjYxOTM3IC0wLjA3NDA4LDAuMDEzMjMgLTAuMTQ1NTIxLDAuMDMxNzUgLTAuMjE2OTU5LDAuMDQ3NjMgYSA3MC43NjY3ODEsNzAuNzY2NzgxIDAgMCAwIC0xNi44NjE4OTYsNi4yODY1IGMgLTAuMzQ2NjA0LDAuMTg1MjA5IC0wLjY4NTI3LDAuMzk0MjI5IC0xLjAzMTg3NSwwLjU4MjA4NCAtMC40MTI3NSwwLjIyNzU0MSAtMC44MjU1LDAuNDYwMzc1IC0xLjIzNTYwNCwwLjY5NTg1NCAtMC4zMDQyNywwLjE3NzI3MSAtMC42MTM4MzMsMC4zMzg2NjYgLTAuOTE4MTA0LDAuNTE4NTgzIC0wLjAwMjYsMC4wMDI2IC0wLjAwNzksMC4wMDUzIC0wLjAxMDU4LDAuMDA1MyAtMC4wMjExNywwLjAxMzIzIC0wLjA0MjMzLDAuMDI2NDYgLTAuMDYzNSwwLjAzNzA0IC0wLjA3OTM3LDAuMDUwMjcgLTAuMTYxMzk2LDAuMDg5OTYgLTAuMjQwNzcxLDAuMTM3NTg0IGwgMC4wMDI2LDAuMDA1MyAwLjczMjg5NiwxLjIxNDQzOCAyMC44MzU5MzcsMzQuNTg4OTc5IGMgMC40MDc0NTksLTAuMjQzNDEgMC44MjI4NTQsLTAuNDY4MzEgMS4yMzgyNSwtMC42OTA1NiAwLjQxODA0MiwtMC4yMjQ4OSAwLjg0MTM3NSwtMC40MzY1NiAxLjI2NzM1NCwtMC42Mzc2NCBhIDI4LjM3NjU2MiwyOC4zNzY1NjIgMCAwIDEgOS4zNTU2NjcsLTIuNjIyMDMgYyAwLjQ3MDk1OCwtMC4wNDc2IDAuOTQxOTE3LC0wLjA4NzMgMS40MTU1MjEsLTAuMTExMTIgMC40NzA5NTgsLTAuMDIzOCAwLjk0NDU2MiwtMC4wNDc2IDEuNDE4MTY3LC0wLjA0NzYgdiAtNDEuNzgwNCBjIC0wLjQ3MzYwNSwwIC0wLjk0NDU2MywwLjAyMTE3IC0xLjQxNTUyMSwwLjAyOTEgeiIKICAgICAgIGZpbGw9IiM2YmE0M2EiCiAgICAgICBpZD0icGF0aDIxIgogICAgICAgc3R5bGU9InN0cm9rZS13aWR0aDowLjI2NDU4MyIgLz4KICAgIDxwYXRoCiAgICAgICBkPSJtIDE1OS43NjY4Miw4NS41NTI2ODQgYyAtMC4wMjEyLC0wLjM3NTcxIC0wLjA0MjMsLTAuNzQ4NzcgLTAuMDY4OCwtMS4xMjcxMiBhIDE5Ljk0OTU4MywxOS45NDk1ODMgMCAwIDAgLTAuMDM3LC0wLjU2MDkyIGMgLTAuMDI5MSwtMC40NDE4NSAtMC4wNjYxLC0wLjg4MzcxIC0wLjEwODQ4LC0xLjMyNTU2IC0wLjAwOCwtMC4xMTM3NyAtMC4wMTg1LC0wLjIyNzU0IC0wLjAyOTEsLTAuMzQxMzIgLTAuMDQ3NiwtMC40OTQ3NyAtMC4xMDA1NCwtMC45ODY4OSAtMC4xNTg3NSwtMS40NzkwMiAtMC4wMDUsLTAuMDM5NyAtMC4wMDgsLTAuMDc5NCAtMC4wMTMyLC0wLjExOTA2IGwgLTAuMDA4LC0wLjA1NTYgYyAtMC4wNjM1LC0wLjUzMTgxIC0wLjEzNDk0LC0xLjA2MzYyIC0wLjIwOTAzLC0xLjU5Mjc5IGwgLTAuMDA4LC0wLjA1MDMgYSA3MC40OTkyODcsNzAuNDk5Mjg3IDAgMCAwIC0yLjE1OSwtOS44NzY5IGMgLTAuMDA4LC0wLjAyMzggLTAuMDEzMiwtMC4wNDIzIC0wLjAyMTIsLTAuMDYzNSAtMC4xNTA4MSwtMC41MDUzNSAtMC4zMDY5MiwtMS4wMDU0MSAtMC40NjMwMiwtMS41MDU0OCAtMC4wMjM4LC0wLjA2NjIgLTAuMDQ1LC0wLjEzMjI5IC0wLjA2NjEsLTAuMTk4NDMgLTAuMTQ4MTYsLTAuNDUyNDQgLTAuMjk4OTgsLTAuOTAyMjMgLTAuNDU1MDgsLTEuMzUyMDIgbCAtMC4xMjcsLTAuMzY1MTMgYyAtMC4xMzc1OCwtMC4zODg5NCAtMC4yODA0NiwtMC43Nzc4NyAtMC40MjMzMywtMS4xNjY4MSAtMC4wNzE0LC0wLjE4NTIxIC0wLjE0MDIzLC0wLjM3MDQyIC0wLjIxNDMyLC0wLjU1Mjk4IC0wLjEyMTcsLTAuMzIyNzkgLTAuMjQ2MDYsLTAuNjQyOTQgLTAuMzc1NywtMC45NjA0NCAtMC4xMDA1NSwtMC4yNTY2NCAtMC4yMDM3MywtMC41MDggLTAuMzA2OTIsLTAuNzU2NzEgLTAuMTAzMTksLTAuMjUxMzUgLTAuMjA5MDIsLTAuNDk3NDEgLTAuMzEyMjEsLTAuNzQ2MTIgLTAuMTM3NTgsLTAuMzE3NSAtMC4yNzc4MSwtMC42MzUgLTAuNDE4MDQsLTAuOTQ5ODYgLTAuMDc5NCwtMC4xNzcyNyAtMC4xNTg3NSwtMC4zNTcxOCAtMC4yNDA3NywtMC41MzcxIC0wLjE3MTk4LC0wLjM3NTcxIC0wLjM0OTI1LC0wLjc1NDA2IC0wLjUyNjUyLC0xLjEyOTc3IC0wLjA1NTYsLTAuMTExMTMgLTAuMTExMTMsLTAuMjI3NTQgLTAuMTY2NjksLTAuMzQxMzEgLTAuMjA2MzcsLTAuNDI4NjMgLTAuNDE4MDQsLTAuODU5OSAtMC42MzUsLTEuMjgzMjMgLTAuMDI5MSwtMC4wNTgyIC0wLjA2MDgsLTAuMTE5MDYgLTAuMDksLTAuMTc3MjcgLTAuMjQwNzcsLTAuNDY4MzIgLTAuNDg5NDgsLTAuOTM5MjcgLTAuNzM4MTksLTEuNDAyMjkgLTAuMDEwNiwtMC4wMTU5IC0wLjAxODUsLTAuMDM0NCAtMC4wMjkxLC0wLjA1MDMgYSA3MC42NDExMDQsNzAuNjQxMTA0IDAgMCAwIC0zLjk5Nzg1LC02LjQ4NzU5NCBsIC0zMC44MzQ0NiwzMC44MzQ1NDQgYyAwLjcwMTE1LDIuMDY2NCAxLjE1NjIzLDQuMTk2MjkgMS4zNzg0OCw2LjM0MjA2IGggNDEuODg4ODQgYyAtMC4wMTA2LC0wLjIwOTAyIC0wLjAxNTksLTAuNDE4MDQgLTAuMDI2NSwtMC42MjE3NyB6IgogICAgICAgZmlsbD0iIzRkNWEzMSIKICAgICAgIGlkPSJwYXRoMjMiCiAgICAgICBzdHlsZT0ic3Ryb2tlLXdpZHRoOjAuMjY0NTgzIiAvPgogICAgPHBhdGgKICAgICAgIGQ9Im0gMTE4LjA2ODQ5LDg5LjAwNTQ5NCBjIDAsMC40NzM2MSAtMC4wNDUsMC45NDQ1NyAtMC4wNjg4LDEuNDE4MTcgLTAuMDIzOCwwLjQ3MzYxIC0wLjAzMTgsMC45NDcyMSAtMC4wNzk0LDEuNDE1NTIgLTAuNTM5NzUsNS40NDI0OCAtMi42Mzc5LDEwLjc1NTMwNiAtNi4yOTk3MywxNS4yMTg4MjYgLTAuMjk2MzQsMC4zNjI0OCAtMC42MzIzNiwwLjcwMTE1IC0wLjk1MjUsMS4wNTMwNCAtMC4zMTc1LDAuMzQ5MjUgLTAuNjExMTksMC43MTE3MyAtMC45NDk4NiwxLjA1MDQgbCAyOS41NDYwMywyOS41NDA3MyBjIDAuMzM2MDIsLTAuMzM2MDIgMC42NTA4NywtMC42ODI2MyAwLjk3ODk1LC0xLjAyMzk0IDAuMzI4MDksLTAuMzQxMzEgMC42NjE0NiwtMC42NzczMyAwLjk3ODk2LC0xLjAyMzk0IDIuMTg1NDYsLTIuMzY4MDIgNC4xODU3MSwtNC44NDQ1MiA1Ljk4NDg4LC03LjQxODkxIDAuMDc0MSwtMC4xMDMxOSAwLjE0MDIzLC0wLjIwMzczIDAuMjExNjYsLTAuMzA0MjcgMC4yMTk2MSwtMC4zMTQ4NiAwLjQzMTI4LC0wLjYzNSAwLjY0ODIzLC0wLjk1MjUgMC4xODc4NiwtMC4yODU3NSAwLjM3NTcxLC0wLjU2ODg2IDAuNTU4MjcsLTAuODU3MjUgMC4wOTI2LC0wLjEzNzU5IDAuMTgyNTcsLTAuMjgwNDYgMC4yNjk4OCwtMC40MTgwNSA2Ljc3NTk4LC0xMC42Mzg4OSAxMC40MDM0MiwtMjIuNjk4NiAxMC44OTAyNSwtMzQuODY0MTM2IDAuMDE4NSwtMC40NzA5NiAwLjAyOTEsLTAuOTQ0NTYgMC4wMzcsLTEuNDE4MTcgMC4wMTA2LC0wLjQ3MDk1IDAuMDIzOCwtMC45NDE5MSAwLjAyMzgsLTEuNDE1NTIgeiIKICAgICAgIGZpbGw9IiM2YmE0M2EiCiAgICAgICBpZD0icGF0aDI1IgogICAgICAgc3R5bGU9InN0cm9rZS13aWR0aDowLjI2NDU4MyIgLz4KICAgIDxwYXRoCiAgICAgICBkPSJtIDE3Mi4yNjA0NCw2LjMxNTI2NDkgYyAtNy42NDM4MSwtNy42NDM4MTIgLTIwLjAzNjg5LC03LjY0MzgxMiAtMjcuNjgwNywwIC02LjA5ODY1LDYuMDk2MDAwMSAtNy4zMTgzOCwxNS4yMTA4OTYxIC0zLjY4NTY1LDIyLjUyOTI3MTEgTCA5OC4yNTM4MzksNzEuNDgyMTQ0IGMgLTcuMzE4Mzc1LC0zLjYzMDA4IC0xNi40MzU5MTcsLTIuNDEzIC0yMi41MzQ1NjMsMy42ODMgLTcuNjQzODEyLDcuNjQzODEgLTcuNjQxMTY2LDIwLjAzNjkgMCwyNy42ODA3MDYgNy42NDY0NTksNy42NDM4MSAyMC4wNDIxODgsNy42NDExNiAyNy42ODYwMDQsMCA2LjA5ODY0LC02LjA5ODY0NiA3LjMxNTczLC0xNS4yMTM1MzYgMy42ODMsLTIyLjUzMTkxNiBMIDE0OS43Mjg1MywzNy42Nzg5NyBjIDcuMzIxMDIsMy42MzAwOSAxNi40MzMyNywyLjQxMyAyMi41MzE5MSwtMy42ODU2NCA3LjY0MzgyLC03LjY0MTE2OSA3LjY0MzgyLC0yMC4wMzQyNTIgMCwtMjcuNjc4MDY1MSB6IgogICAgICAgZmlsbD0iIzQyNDE0MyIKICAgICAgIGlkPSJwYXRoMjciCiAgICAgICBzdHlsZT0ic3Ryb2tlLXdpZHRoOjAuMjY0NTgzIiAvPgogIDwvZz4KPC9zdmc+Cg=='); + background-size: 100%; +} + #btnSend { height: 42px; diff --git a/server/website/index.html b/server/website/index.html index 0c553a0..0a29f1a 100644 --- a/server/website/index.html +++ b/server/website/index.html @@ -21,7 +21,9 @@ - + + + API

Simple Cloud Notifier

@@ -36,6 +38,11 @@
+
+
+
+
+
@@ -48,13 +55,13 @@
-
-
+
+
-
-
+
+
diff --git a/server/website/js/logic.js b/server/website/js/logic.js index 756a965..53b03b3 100644 --- a/server/website/js/logic.js +++ b/server/website/js/logic.js @@ -10,22 +10,24 @@ function send() let uid = document.getElementById("uid"); let key = document.getElementById("ukey"); - let msg = document.getElementById("msg"); - let txt = document.getElementById("txt"); + let tit = document.getElementById("tit"); + let cnt = document.getElementById("cnt"); let pio = document.getElementById("prio"); + let cha = document.getElementById("chan"); uid.classList.remove('input-invalid'); key.classList.remove('input-invalid'); msg.classList.remove('input-invalid'); - txt.classList.remove('input-invalid'); + cnt.classList.remove('input-invalid'); pio.classList.remove('input-invalid'); let data = new FormData(); data.append('user_id', uid.value); data.append('user_key', key.value); - data.append('title', msg.value); - data.append('content', txt.value); - data.append('priority', pio.value); + if (tit.value !== '') data.append('title', tit.value); + if (cnt.value !== '') data.append('content', cnt.value); + if (pio.value !== '') data.append('priority', pio.value); + if (cha.value !== '') data.append('channel', cha.value); let xhr = new XMLHttpRequest(); xhr.open('POST', '/send.php', true); @@ -41,9 +43,10 @@ function send() { if (resp.errhighlight === 101) uid.classList.add('input-invalid'); if (resp.errhighlight === 102) key.classList.add('input-invalid'); - if (resp.errhighlight === 103) msg.classList.add('input-invalid'); - if (resp.errhighlight === 104) txt.classList.add('input-invalid'); + if (resp.errhighlight === 103) tit.classList.add('input-invalid'); + if (resp.errhighlight === 104) cnt.classList.add('input-invalid'); if (resp.errhighlight === 105) pio.classList.add('input-invalid'); + if (resp.errhighlight === 106) cha.classList.add('input-invalid'); Toastify({ text: resp.message, @@ -62,7 +65,8 @@ function send() '"a_remain=' + (resp.quota_max-resp.quota) + '"a_max=' + resp.quota_max + '&preset_user_id=' + uid.value + - '&preset_user_key=' + key.value; + '&preset_user_key=' + key.value + + '&preset_channel=' + cha.value; } } else @@ -85,23 +89,21 @@ window.addEventListener("load", function () { const qp = new URLSearchParams(window.location.search); - const btnSend = document.getElementById("btnSend"); - const selPrio = document.getElementById("prio"); - const txtKey = document.getElementById("ukey"); - const txtUID = document.getElementById("uid"); - const txtTitl = document.getElementById("msg"); - const txtCont = document.getElementById("txt"); + let btn = document.getElementById("btnSend"); + let uid = document.getElementById("uid"); + let key = document.getElementById("ukey"); + let tit = document.getElementById("tit"); + let cnt = document.getElementById("cnt"); + let pio = document.getElementById("prio"); + let cha = document.getElementById("chan"); - btnSend.onclick = function () { send(); return false; }; + btn.onclick = function () { send(); return false; }; - if (qp.has('preset_priority')) selPrio.selectedIndex = parseInt(qp.get("preset_priority")); - - if (qp.has('preset_user_key')) txtKey.value = qp.get("preset_user_key"); - - if (qp.has('preset_user_id')) txtUID.value = qp.get("preset_user_id"); - - if (qp.has('preset_title')) txtTitl.value = qp.get("preset_title"); - - if (qp.has('preset_content')) txtCont.value = qp.get("preset_content"); + if (qp.has('preset_priority')) pio.selectedIndex = parseInt(qp.get("preset_priority")); + if (qp.has('preset_user_key')) key.value = qp.get("preset_user_key"); + if (qp.has('preset_user_id')) uid.value = qp.get("preset_user_id"); + if (qp.has('preset_title')) tit.value = qp.get("preset_title"); + if (qp.has('preset_content')) cnt.value = qp.get("preset_content"); + if (qp.has('preset_channel')) cha.value = qp.get("preset_channel"); }, false); \ No newline at end of file diff --git a/server/website/js/message_sent.js b/server/website/js/message_sent.js index 28dfdc9..a15b524 100644 --- a/server/website/js/message_sent.js +++ b/server/website/js/message_sent.js @@ -14,7 +14,10 @@ window.addEventListener("load", function () const preset_user_id = qp.get('preset_user_id') ?? 'ERR'; const preset_user_key = qp.get('preset_user_key') ?? 'ERR'; - linkSucc.setAttribute("href", "/?preset_user_id="+preset_user_id+"&preset_user_key="+preset_user_key); + let hrefBack = "/?preset_user_id="+preset_user_id+"&preset_user_key="+preset_user_key + if (qp.has('preset_channel') && qp.get('preset_channel') !== '') hrefBack += "&preset_channel="+qp.get('preset_channel') + + linkSucc.setAttribute("href", hrefBack); if (qp.get("ok") === "1") { diff --git a/server/website/message_sent.html b/server/website/message_sent.html index ca84126..ade52e6 100644 --- a/server/website/message_sent.html +++ b/server/website/message_sent.html @@ -20,9 +20,16 @@
+ + + + Send + +

Simple Cloud Notifier

+ diff --git a/server/website/scn_send.html b/server/website/scn_send.html new file mode 100644 index 0000000..008c37b --- /dev/null +++ b/server/website/scn_send.html @@ -0,0 +1,131 @@ + + + +#!/usr/bin/env bash + +# +# Wrapper around SCN ( https://scn.blackforestbytes.com/ ) +# ======================================================== +# +# ./scn_send [@channel] title [content] [priority] +# +# +# Call with scn_send "${title}" +# or scn_send "${title}" ${content}" +# or scn_send "${title}" ${content}" "${priority:0|1|2}" +# or scn_send "@${channel} "${title}" +# or scn_send "@${channel} "${title}" ${content}" +# or scn_send "@${channel} "${title}" ${content}" "${priority:0|1|2}" +# +# + +################################################################################ +# INSERT YOUR DATA HERE # +################################################################################ +user_id="999" +user_key="??" +################################################################################ + +usage() { + echo "Usage: " + echo " scn_send [@channel] title [content] [priority]" + echo "" +} + +args=( "$@" ) + +title=$1 +content="" +channel="" +priority=1 +usr_msg_id=$(uuidgen) +sendtime=$(date +%s) + +if [ ${#args[@]} -lt 1 ]; then + echo "[ERROR]: no title supplied via parameter" 1>&2 + usage + exit 1 +fi + +if [[ "${args[0]}" =~ ^@.* ]]; then + channel="${args[0]}" + unset "args[0]" + channel="${channel:1}" +fi + +if [ ${#args[@]} -lt 1 ]; then + echo "[ERROR]: no title supplied via parameter" 1>&2 + usage + exit 1 +fi + +title="${args[0]}" +content="" +sendtime=$(date +%s) + +if [ ${#args[@]} -gt 1 ]; then + content="${args[0]}" + unset "args[0]" +fi + +if [ ${#args[@]} -gt 1 ]; then + priority="${args[0]}" + unset "args[0]" +fi + +if [ ${#args[@]} -gt 1 ]; then + echo "Too many arguments to scn_send" 1>&2 + usage + exit 1 +fi + + +while true ; do + + curlresp=$(curl --silent \ + --output /dev/null \ + --write-out "%{http_code}" \ + --data "user_id=$user_id" \ + --data "user_key=$user_key" \ + --data "title=$title" \ + --data "timestamp=$sendtime" \ + --data "content=$content" \ + --data "priority=$priority" \ + --data "msg_id=$usr_msg_id" \ + --data "channel=$channel" \ + "https://scn.blackforestbytes.com/" ) + + if [ "$curlresp" == 200 ] ; then + echo "Successfully send" + exit 0 + fi + + if [ "$curlresp" == 400 ] ; then + echo "Bad request - something went wrong" 1>&2 + exit 1 + fi + + if [ "$curlresp" == 401 ] ; then + echo "Unauthorized - wrong userid/userkey" 1>&2 + exit 1 + fi + + if [ "$curlresp" == 403 ] ; then + echo "Quota exceeded - wait one hour before re-try" 1>&2 + sleep 3600 + fi + + if [ "$curlresp" == 412 ] ; then + echo "Precondition Failed - No device linked" 1>&2 + exit 1 + fi + + if [ "$curlresp" == 500 ] ; then + echo "Internal server error - waiting for better times" 1>&2 + sleep 60 + fi + + # if none of the above matched we probably hav no network ... + echo "Send failed (response code $curlresp) ... try again in 5s" 1>&2 + sleep 5 +done
403 (Forbidden) The user has exceeded its daily quota - wait 24 hours or upgrade your account
412 (Precondition Failed)There is no device connected with this account - open the app and press the refresh button in the account tab
500 (Internal Server Error) There was an internal error while sending your data - try again later