Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
Julian Graf | 77cfe75043 | |
Mike Schwörer | 51f5f1005a | |
Mike Schwörer | 0a380f861e | |
Mike Schwörer | b712ad3488 | |
Mike Schwörer | 9f656bdefe | |
Mike Schwörer | a4a651229c | |
Mike Schwörer | 4773800f23 | |
Julian Graf | bef0b8189e | |
Mike Schwörer | 674714f0f3 | |
Mike Schwörer | ee9e858584 | |
Mike Schwörer | 165c6d8614 | |
Mike Schwörer | 8a6719fc19 | |
Mike Schwörer | 308361a834 | |
Mike Schwörer | 44df964f6f | |
Mike Schwörer | 56bf266919 | |
Mike Schwörer | f3658d6636 |
|
@ -0,0 +1,43 @@
|
|||
|
||||
# https://docs.gitea.com/next/usage/actions/quickstart
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||
|
||||
name: Build Docker and Deploy
|
||||
run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['master']
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
build_job:
|
||||
name: Build Docker Container
|
||||
runs-on: bfb-cicd-latest
|
||||
steps:
|
||||
- run: echo -n "${{ secrets.DOCKER_REG_PASS }}" | docker login registry.blackforestbytes.com -u docker --password-stdin
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
- run: cd "${{ gitea.workspace }}/scnserver" && make clean
|
||||
- run: cd "${{ gitea.workspace }}/scnserver" && make docker
|
||||
- run: cd "${{ gitea.workspace }}/scnserver" && make push-docker
|
||||
|
||||
deploy_job:
|
||||
name: Deploy to Server
|
||||
needs: [build_job]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Execute deploy on remote (via ssh)
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
with:
|
||||
host: simplecloudnotifier.de
|
||||
username: bfb-deploy-bot
|
||||
port: 4477
|
||||
key: "${{ secrets.SSH_KEY_BFBDEPLOYBOT }}"
|
||||
script: cd /var/docker/deploy-scripts/simplecloudnotifier && ./deploy.sh master "${{ gitea.sha }}" || exit 1
|
||||
|
||||
|
||||
|
||||
|
|
@ -14,13 +14,10 @@
|
|||
# or scn_send "@${channel} "${title}" ${content}"
|
||||
# or scn_send "@${channel} "${title}" ${content}" "${priority:0|1|2}"
|
||||
#
|
||||
# content can be of format "--scnsend-read-body-from-file={path}" to read body from file
|
||||
# (this circumvents max commandline length)
|
||||
#
|
||||
|
||||
################################################################################
|
||||
# INSERT YOUR DATA HERE #
|
||||
################################################################################
|
||||
user_id="999" # your user_id
|
||||
user_key="??" # use userkey with SEND permissions on the used channel
|
||||
################################################################################
|
||||
|
||||
usage() {
|
||||
|
@ -34,16 +31,40 @@ function cfgcol { [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge
|
|||
function rederr() { if cfgcol; then >&2 echo -e "\x1B[31m$1\x1B[0m"; else >&2 echo "$1"; fi; }
|
||||
function green() { if cfgcol; then echo -e "\x1B[32m$1\x1B[0m"; else echo "$1"; fi; }
|
||||
|
||||
################################################################################
|
||||
|
||||
#
|
||||
# Get env 'SCN_UID' and 'SCN_KEY' from conf file
|
||||
#
|
||||
# shellcheck source=/dev/null
|
||||
. "/etc/scn.conf"
|
||||
SCN_UID=${SCN_UID:-}
|
||||
SCN_KEY=${SCN_KEY:-}
|
||||
|
||||
[ -z "${SCN_UID}" ] && { rederr "Missing config value 'SCN_UID' in /etc/scn.conf"; exit 1; }
|
||||
[ -z "${SCN_KEY}" ] && { rederr "Missing config value 'SCN_KEY' in /etc/scn.conf"; exit 1; }
|
||||
|
||||
################################################################################
|
||||
|
||||
args=( "$@" )
|
||||
|
||||
title=""
|
||||
content=""
|
||||
channel=""
|
||||
priority=1
|
||||
priority=""
|
||||
usr_msg_id="$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)"
|
||||
sendtime="$(date +%s)"
|
||||
sender="$(hostname)"
|
||||
|
||||
if command -v srvname &> /dev/null; then
|
||||
sender="$( srvname )"
|
||||
fi
|
||||
|
||||
if [[ "${args[0]}" = "--" ]]; then
|
||||
# only positional args form here on (currently not handled)
|
||||
args=("${args[@]:1}")
|
||||
fi
|
||||
|
||||
if [ ${#args[@]} -lt 1 ]; then
|
||||
rederr "[ERROR]: no title supplied via parameter"
|
||||
usage
|
||||
|
@ -83,28 +104,33 @@ if [ ${#args[@]} -gt 0 ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$content" == --scnsend-read-body-from-file=* ]]; then
|
||||
path="$( awk '{ print substr($0, 31) }' <<< "$content" )"
|
||||
content="$( cat "$path" )"
|
||||
fi
|
||||
|
||||
curlparams=()
|
||||
|
||||
curlparams+=( "--data" "user_id=${SCN_UID}" )
|
||||
curlparams+=( "--data" "key=${SCN_KEY}" )
|
||||
curlparams+=( "--data" "title=$title" )
|
||||
curlparams+=( "--data" "timestamp=$sendtime" )
|
||||
curlparams+=( "--data" "msg_id=$usr_msg_id" )
|
||||
curlparams+=( "--data-urlencode" "user_id=${SCN_UID}" )
|
||||
curlparams+=( "--data-urlencode" "key=${SCN_KEY}" )
|
||||
curlparams+=( "--data-urlencode" "title=$title" )
|
||||
curlparams+=( "--data-urlencode" "timestamp=$sendtime" )
|
||||
curlparams+=( "--data-urlencode" "msg_id=$usr_msg_id" )
|
||||
|
||||
if [[ -n "$content" ]]; then
|
||||
curlparams+=("--data" "content=$content")
|
||||
curlparams+=("--data-urlencode" "content=$content")
|
||||
fi
|
||||
|
||||
if [[ -n "$priority" ]]; then
|
||||
curlparams+=("--data" "priority=$priority")
|
||||
curlparams+=("--data-urlencode" "priority=$priority")
|
||||
fi
|
||||
|
||||
if [[ -n "$channel" ]]; then
|
||||
curlparams+=("--data" "channel=$channel")
|
||||
curlparams+=("--data-urlencode" "channel=$channel")
|
||||
fi
|
||||
|
||||
if [[ -n "$sender" ]]; then
|
||||
curlparams+=("--data" "sender_name=$sender")
|
||||
curlparams+=("--data-urlencode" "sender_name=$sender")
|
||||
fi
|
||||
|
||||
while true ; do
|
|
@ -18,6 +18,10 @@ identifier.sqlite
|
|||
|
||||
.idea/dataSources.xml
|
||||
|
||||
.swaggobin
|
||||
|
||||
scn_send.sh
|
||||
|
||||
##############
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GoLinterSettings">
|
||||
<option name="checkGoLinterExe" value="false" />
|
||||
</component>
|
||||
</project>
|
|
@ -4,11 +4,13 @@ FROM golang:1-bullseye AS builder
|
|||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ca-certificates openssl make git tar coreutils && \
|
||||
apt-get install -y python3 python3-pip && \
|
||||
pip install virtualenv && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . /buildsrc
|
||||
|
||||
RUN cd /buildsrc && make build
|
||||
RUN cd /buildsrc && cp "scn_send.sh" "../scn_send.sh" && make build
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ HASH=$(shell git rev-parse HEAD)
|
|||
|
||||
.PHONY: test swagger pygmentize docker migrate dgi pygmentize lint docker
|
||||
|
||||
SWAGGO_VERSION=v1.8.12
|
||||
SWAGGO=github.com/swaggo/swag/cmd/swag@$(SWAGGO_VERSION)
|
||||
|
||||
build: swagger pygmentize fmt
|
||||
mkdir -p _build
|
||||
rm -f ./_build/scn_backend
|
||||
|
@ -30,6 +33,7 @@ dgi:
|
|||
echo -n "REMOTE=" >> DOCKER_GIT_INFO ; git config --get remote.origin.url >> DOCKER_GIT_INFO
|
||||
|
||||
docker: dgi
|
||||
cp ../scn_send.sh .
|
||||
docker build \
|
||||
-t "$(DOCKER_NAME):$(HASH)" \
|
||||
-t "$(DOCKER_NAME):$(NAMESPACE)-latest" \
|
||||
|
@ -38,17 +42,21 @@ docker: dgi
|
|||
-t "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest" \
|
||||
-t "$(DOCKER_REPO)/$(DOCKER_NAME):latest" \
|
||||
.
|
||||
[ -f "scn_send.sh" ] && rm scn_send.sh
|
||||
|
||||
swagger:
|
||||
which swag || go install github.com/swaggo/swag/cmd/swag@v1.8.12
|
||||
swag init -generalInfo api/router.go --propertyStrategy snakecase --output ./swagger/ --outputTypes "json,yaml"
|
||||
swagger-setup:
|
||||
mkdir -p ".swaggobin"
|
||||
[ -f ".swaggobin/swag_$(SWAGGO_VERSION)" ] || { GOBIN=/tmp/_swaggo go install $(SWAGGO); cp "/tmp/_swaggo/swag" ".swaggobin/swag_$(SWAGGO_VERSION)"; rm -rf "/tmp/_swaggo"; }
|
||||
|
||||
swagger: swagger-setup
|
||||
".swaggobin/swag_$(SWAGGO_VERSION)" init -generalInfo ./api/router.go --propertyStrategy camelcase --output ./swagger/ --outputTypes "json,yaml"
|
||||
|
||||
pygmentize: website/scn_send.html
|
||||
|
||||
website/scn_send.html: website/scn_send.sh.txt
|
||||
_pygments/pygmentizew -l bash -f html "$(shell pwd)/website/scn_send.sh.txt" > "$(shell pwd)/website/scn_send.html"
|
||||
_pygments/pygmentizew -S monokai -f html > "$(shell pwd)/website/css/pygmnetize-dark.css"
|
||||
_pygments/pygmentizew -S borland -f html > "$(shell pwd)/website/css/pygmnetize-light.css"
|
||||
website/scn_send.html: ../scn_send.sh
|
||||
_pygments/pygmentizew -l bash -f html "$(shell pwd)/../scn_send.sh" > "$(shell pwd)/website/scn_send.html"
|
||||
_pygments/pygmentizew -S monokai -f html > "$(shell pwd)/website/css/pygmnetize-dark.css"
|
||||
_pygments/pygmentizew -S borland -f html > "$(shell pwd)/website/css/pygmnetize-light.css"
|
||||
|
||||
run-docker-local: docker
|
||||
mkdir -p .run-data
|
||||
|
@ -67,7 +75,7 @@ inspect-docker: docker
|
|||
$(DOCKER_NAME):latest \
|
||||
bash
|
||||
|
||||
push-docker: docker
|
||||
push-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"
|
||||
|
@ -75,13 +83,14 @@ push-docker: docker
|
|||
clean:
|
||||
rm -rf _build/*
|
||||
rm -rf .run-data/*
|
||||
rm -rf _pygments/env
|
||||
git clean -fdx
|
||||
go clean
|
||||
go clean -testcache
|
||||
! which go 2>&1 >> /dev/null || go clean
|
||||
! which go 2>&1 >> /dev/null || go clean -testcache
|
||||
|
||||
fmt:
|
||||
fmt: swagger-setup
|
||||
go fmt ./...
|
||||
swag fmt
|
||||
".swaggobin/swag_$(SWAGGO_VERSION)" fmt
|
||||
|
||||
test:
|
||||
which gotestsum || go install gotest.tools/gotestsum@latest
|
||||
|
|
|
@ -4,14 +4,18 @@
|
|||
========
|
||||
|
||||
|
||||
#### BEFORE RELEASE
|
||||
#### DO DO DO
|
||||
|
||||
- app-store link in HTML
|
||||
|
||||
- backups (no longer container in my db_backup, perhaps extend it to sqlite?)
|
||||
|
||||
- ios purchase verification
|
||||
|
||||
- (!) use goext.ginWrapper
|
||||
|
||||
- (!) use goext.exerr
|
||||
|
||||
- use bfcodegen (enums+id)
|
||||
|
||||
#### UNSURE
|
||||
|
||||
- (?) default-priority for channels
|
||||
|
@ -47,10 +51,6 @@
|
|||
|
||||
- route to re-check all pro-token (for me)
|
||||
|
||||
- /send endpoint should be compatible with the [ webhook ] notifier of uptime-kuma
|
||||
(or add another /kuma endpoint)
|
||||
-> https://webhook.site/
|
||||
|
||||
- endpoint to list all servernames of user (distinct select)
|
||||
|
||||
- weblogin, webapp, ...
|
||||
|
@ -64,6 +64,10 @@
|
|||
|
||||
- use job superclass (copy from isi/bnet/?), reduce duplicate code
|
||||
|
||||
- admin panel (especially errors and requests)
|
||||
|
||||
- cli app (?)
|
||||
|
||||
#### FUTURE
|
||||
|
||||
- Remove compat, especially do not create compat id for every new message...
|
||||
|
|
|
@ -4,8 +4,10 @@ import (
|
|||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/impl/primary"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
|
@ -146,7 +148,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -179,6 +181,8 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||
type body struct {
|
||||
Name string `json:"name"`
|
||||
Subscribe *bool `json:"subscribe"`
|
||||
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
var u uri
|
||||
|
@ -206,7 +210,7 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, u.UserID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -232,7 +236,15 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||
|
||||
subscribeKey := h.app.GenerateRandomAuthKey()
|
||||
|
||||
channel, err := h.database.CreateChannel(ctx, u.UserID, channelDisplayName, channelInternalName, subscribeKey)
|
||||
cChannel := primary.CreateChanel{
|
||||
UserId: u.UserID,
|
||||
IntName: channelInternalName,
|
||||
SubscribeKey: subscribeKey,
|
||||
DisplayName: channelDisplayName,
|
||||
Description: b.Description,
|
||||
}
|
||||
|
||||
channel, err := h.database.CreateChannel(ctx, cChannel)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err)
|
||||
}
|
||||
|
@ -298,7 +310,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
_, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -306,7 +318,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, u.UserID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -348,8 +360,8 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||
descName = langext.Ptr(strings.TrimSpace(*b.DescriptionName))
|
||||
}
|
||||
|
||||
if descName != nil && len(*descName) > user.MaxChannelDescriptionNameLength() {
|
||||
return ginresp.APIError(g, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG, fmt.Sprintf("Channel-Description too long (max %d characters)", user.MaxChannelNameLength()), nil)
|
||||
if descName != nil && len(*descName) > user.MaxChannelDescriptionLength() {
|
||||
return ginresp.APIError(g, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG, fmt.Sprintf("Channel-Description too long (max %d characters)", user.MaxChannelDescriptionLength()), nil)
|
||||
}
|
||||
|
||||
err := h.database.UpdateChannelDescriptionName(ctx, u.ChannelID, descName)
|
||||
|
@ -420,7 +432,7 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
|
|||
pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize)
|
||||
|
||||
channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID, false)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"net/http"
|
||||
|
@ -87,7 +88,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -192,7 +193,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -251,7 +252,7 @@ func (h APIHandler) UpdateClient(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"net/http"
|
||||
|
@ -90,7 +91,7 @@ func (h APIHandler) GetUserKey(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -143,7 +144,7 @@ func (h APIHandler) UpdateUserKey(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -302,7 +303,7 @@ func (h APIHandler) DeleteUserKey(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
|
||||
"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"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ListMessages swaggerdoc
|
||||
|
@ -173,7 +175,7 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
|
|||
// @Failure 404 {object} ginresp.apiError "message not found"
|
||||
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||
//
|
||||
// @Router /api/v2/messages/{mid} [PATCH]
|
||||
// @Router /api/v2/messages/{mid} [GET]
|
||||
func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||
type uri struct {
|
||||
MessageID models.MessageID `uri:"mid" binding:"entityid"`
|
||||
|
@ -191,7 +193,7 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
msg, err := h.database.GetMessage(ctx, u.MessageID, false)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -259,14 +261,14 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
msg, err := h.database.GetMessage(ctx, u.MessageID, false)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err)
|
||||
}
|
||||
|
||||
if !ctx.CheckPermissionMessageRead(msg) {
|
||||
if !ctx.CheckPermissionMessageDelete(msg) {
|
||||
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"net/http"
|
||||
|
@ -14,13 +15,25 @@ import (
|
|||
// ListUserSubscriptions swaggerdoc
|
||||
//
|
||||
// @Summary List all subscriptions of a user (incoming/owned)
|
||||
// @Description The possible values for 'selector' are:
|
||||
// @Description - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)
|
||||
// @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber
|
||||
// @Description - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber
|
||||
// @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
|
||||
// @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
|
||||
// @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
|
||||
//
|
||||
// @Description The possible values for 'direction' are:
|
||||
// @Description - "outgoing" Subscriptions with the user as subscriber (= subscriptions he can use to read channels)
|
||||
// @Description - "incoming" Subscriptions to channels of this user (= incoming subscriptions and subscription requests)
|
||||
// @Description - "both" Combines "outgoing" and "incoming" (default)
|
||||
// @Description
|
||||
// @Description The possible values for 'confirmation' are:
|
||||
// @Description - "confirmed" Confirmed (active) subscriptions
|
||||
// @Description - "unconfirmed" Unconfirmed (pending) subscriptions
|
||||
// @Description - "all" Combines "confirmed" and "unconfirmed" (default)
|
||||
// @Description
|
||||
// @Description The possible values for 'external' are:
|
||||
// @Description - "true" Subscriptions with subscriber_user_id != channel_owner_user_id (subscriptions from other users)
|
||||
// @Description - "false" Subscriptions with subscriber_user_id == channel_owner_user_id (subscriptions from this user to his own channels)
|
||||
// @Description - "all" Combines "external" and "internal" (default)
|
||||
// @Description
|
||||
// @Description The `subscriber_user_id` parameter can be used to additionally filter the subscriber_user_id (return subscribtions from a specific user)
|
||||
// @Description
|
||||
// @Description The `channel_owner_user_id` parameter can be used to additionally filter the channel_owner_user_id (return subscribtions to a specific user)
|
||||
//
|
||||
// @ID api-user-subscriptions-list
|
||||
// @Tags API-v2
|
||||
|
@ -39,7 +52,11 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
|
|||
UserID models.UserID `uri:"uid" binding:"entityid"`
|
||||
}
|
||||
type query struct {
|
||||
Selector *string `json:"selector" form:"selector" enums:"outgoing_all,outgoing_confirmed,outgoing_unconfirmed,incoming_all,incoming_confirmed,incoming_unconfirmed"`
|
||||
Direction *string `json:"direction" form:"direction" enums:"incoming,outgoing,both"`
|
||||
Confirmation *string `json:"confirmation" form:"confirmation" enums:"confirmed,unconfirmed,all"`
|
||||
External *string `json:"external" form:"external" enums:"true,false,all"`
|
||||
SubscriberUserID *models.UserID `json:"subscriber_user_id" form:"subscriber_user_id"`
|
||||
ChannelOwnerUserID *models.UserID `json:"channel_owner_user_id" form:"channel_owner_user_id"`
|
||||
}
|
||||
type response struct {
|
||||
Subscriptions []models.SubscriptionJSON `json:"subscriptions"`
|
||||
|
@ -57,57 +74,56 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
|
|||
return *permResp
|
||||
}
|
||||
|
||||
sel := strings.ToLower(langext.Coalesce(q.Selector, "outgoing_all"))
|
||||
filter := models.SubscriptionFilter{}
|
||||
filter.AnyUserID = langext.Ptr(u.UserID)
|
||||
|
||||
var res []models.Subscription
|
||||
var err error
|
||||
|
||||
if sel == "outgoing_all" {
|
||||
|
||||
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||
if q.Direction != nil {
|
||||
if strings.EqualFold(*q.Direction, "incoming") {
|
||||
filter.ChannelOwnerUserID = langext.Ptr([]models.UserID{u.UserID})
|
||||
} else if strings.EqualFold(*q.Direction, "outgoing") {
|
||||
filter.SubscriberUserID = langext.Ptr([]models.UserID{u.UserID})
|
||||
} else if strings.EqualFold(*q.Direction, "both") {
|
||||
// both
|
||||
} else {
|
||||
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'direction'", nil)
|
||||
}
|
||||
}
|
||||
|
||||
} else if sel == "outgoing_confirmed" {
|
||||
|
||||
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true))
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||
if q.Confirmation != nil {
|
||||
if strings.EqualFold(*q.Confirmation, "confirmed") {
|
||||
filter.Confirmed = langext.PTrue
|
||||
} else if strings.EqualFold(*q.Confirmation, "unconfirmed") {
|
||||
filter.Confirmed = langext.PFalse
|
||||
} else if strings.EqualFold(*q.Confirmation, "all") {
|
||||
// both
|
||||
} else {
|
||||
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'confirmation'", nil)
|
||||
}
|
||||
}
|
||||
|
||||
} else if sel == "outgoing_unconfirmed" {
|
||||
|
||||
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false))
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||
if q.External != nil {
|
||||
if strings.EqualFold(*q.External, "true") {
|
||||
filter.SubscriberIsChannelOwner = langext.PFalse
|
||||
} else if strings.EqualFold(*q.External, "false") {
|
||||
filter.SubscriberIsChannelOwner = langext.PTrue
|
||||
} else if strings.EqualFold(*q.External, "all") {
|
||||
// both
|
||||
} else {
|
||||
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'external'", nil)
|
||||
}
|
||||
}
|
||||
|
||||
} else if sel == "incoming_all" {
|
||||
if q.SubscriberUserID != nil {
|
||||
filter.SubscriberUserID2 = langext.Ptr([]models.UserID{*q.SubscriberUserID})
|
||||
}
|
||||
|
||||
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, nil)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||
}
|
||||
|
||||
} else if sel == "incoming_confirmed" {
|
||||
|
||||
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(true))
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||
}
|
||||
|
||||
} else if sel == "incoming_unconfirmed" {
|
||||
|
||||
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(false))
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
|
||||
if q.ChannelOwnerUserID != nil {
|
||||
filter.ChannelOwnerUserID2 = langext.Ptr([]models.UserID{*q.ChannelOwnerUserID})
|
||||
}
|
||||
|
||||
res, err := h.database.ListSubscriptions(ctx, filter)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||
}
|
||||
|
||||
jsonres := langext.ArrMap(res, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() })
|
||||
|
@ -152,14 +168,14 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons
|
|||
}
|
||||
|
||||
_, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
||||
}
|
||||
|
||||
clients, err := h.database.ListSubscriptionsByChannel(ctx, u.ChannelID)
|
||||
clients, err := h.database.ListSubscriptions(ctx, models.SubscriptionFilter{AnyUserID: langext.Ptr(u.UserID), ChannelID: langext.Ptr([]models.ChannelID{u.ChannelID})})
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||
}
|
||||
|
@ -203,7 +219,7 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -250,7 +266,7 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -414,7 +430,7 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
|
|||
userid := *ctx.GetPermissionUserID()
|
||||
|
||||
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -167,7 +168,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, u.UserID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
@ -3,6 +3,7 @@ package handler
|
|||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
|
||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||
"bytes"
|
||||
"context"
|
||||
|
@ -127,7 +128,9 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
|
|||
return ginresp.InternalError(errors.New("sqlite version too low"))
|
||||
}
|
||||
|
||||
err := h.app.Database.Ping(ctx)
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
err := h.app.Database.Ping(tctx)
|
||||
if err != nil {
|
||||
return ginresp.InternalError(err)
|
||||
}
|
||||
|
@ -137,12 +140,12 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
|
|||
uuidKey, _ := langext.NewHexUUID()
|
||||
uuidWrite, _ := langext.NewHexUUID()
|
||||
|
||||
err = subdb.WriteMetaString(ctx, uuidKey, uuidWrite)
|
||||
err = subdb.WriteMetaString(tctx, uuidKey, uuidWrite)
|
||||
if err != nil {
|
||||
return ginresp.InternalError(err)
|
||||
}
|
||||
|
||||
uuidRead, err := subdb.ReadMetaString(ctx, uuidKey)
|
||||
uuidRead, err := subdb.ReadMetaString(tctx, uuidKey)
|
||||
if err != nil {
|
||||
return ginresp.InternalError(err)
|
||||
}
|
||||
|
@ -151,7 +154,7 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
|
|||
return ginresp.InternalError(errors.New("writing into DB was not consistent"))
|
||||
}
|
||||
|
||||
err = subdb.DeleteMeta(ctx, uuidKey)
|
||||
err = subdb.DeleteMeta(tctx, uuidKey)
|
||||
if err != nil {
|
||||
return ginresp.InternalError(err)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
|
||||
|
@ -28,7 +29,7 @@ func NewCompatHandler(app *logic.Application) CompatHandler {
|
|||
}
|
||||
}
|
||||
|
||||
// SendMessageCompat swaggerdoc
|
||||
// SendMessage swaggerdoc
|
||||
//
|
||||
// @Deprecated
|
||||
//
|
||||
|
@ -36,17 +37,17 @@ func NewCompatHandler(app *logic.Application) CompatHandler {
|
|||
// @Description All parameter can be set via query-parameter or form-data body. Only UserID, UserKey and Title are required
|
||||
// @Tags External
|
||||
//
|
||||
// @Param query_data query handler.SendMessageCompat.combined false " "
|
||||
// @Param form_data formData handler.SendMessageCompat.combined false " "
|
||||
// @Param query_data query handler.SendMessage.combined false " "
|
||||
// @Param form_data formData handler.SendMessage.combined false " "
|
||||
//
|
||||
// @Success 200 {object} handler.SendMessageCompat.response
|
||||
// @Success 200 {object} handler.SendMessage.response
|
||||
// @Failure 400 {object} ginresp.apiError
|
||||
// @Failure 401 {object} ginresp.apiError
|
||||
// @Failure 403 {object} ginresp.apiError
|
||||
// @Failure 500 {object} ginresp.apiError
|
||||
//
|
||||
// @Router /send.php [POST]
|
||||
func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
|
||||
func (h CompatHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||
type combined struct {
|
||||
UserID *int64 `json:"user_id" form:"user_id"`
|
||||
UserKey *string `json:"user_key" form:"user_key"`
|
||||
|
@ -87,7 +88,7 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
|
|||
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil)
|
||||
}
|
||||
|
||||
okResp, errResp := h.sendMessageInternal(g, ctx, langext.Ptr(models.UserID(*newid)), data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil)
|
||||
okResp, errResp := h.app.SendMessage(g, ctx, langext.Ptr(models.UserID(*newid)), data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil)
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
} else {
|
||||
|
@ -287,7 +288,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(201, "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -295,7 +296,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(204, "Authentification failed")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -311,7 +312,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
filter := models.MessageFilter{
|
||||
Owner: langext.Ptr([]models.UserID{user.UserID}),
|
||||
Sender: langext.Ptr([]models.UserID{user.UserID}),
|
||||
CompatAcknowledged: langext.Ptr(false),
|
||||
}
|
||||
|
||||
|
@ -395,7 +396,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(201, "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -403,7 +404,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(204, "Authentification failed")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -497,7 +498,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(201, "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -505,7 +506,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(204, "Authentification failed")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -516,7 +517,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
filter := models.MessageFilter{
|
||||
Owner: langext.Ptr([]models.UserID{user.UserID}),
|
||||
Sender: langext.Ptr([]models.UserID{user.UserID}),
|
||||
CompatAcknowledged: langext.Ptr(false),
|
||||
}
|
||||
|
||||
|
@ -528,7 +529,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
|
|||
compMsgs := make([]models.CompatMessage, 0, len(msgs))
|
||||
for _, v := range msgs {
|
||||
|
||||
messageIdComp, err := h.database.ConvertToCompatIDOrCreate(ctx, v.MessageID.String(), "messageid")
|
||||
messageIdComp, err := h.database.ConvertToCompatIDOrCreate(ctx, "messageid", v.MessageID.String())
|
||||
if err != nil {
|
||||
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create messageid<old>", err)
|
||||
}
|
||||
|
@ -614,7 +615,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(201, "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -622,7 +623,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(204, "Authentification failed")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -744,7 +745,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(201, "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -752,7 +753,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(204, "Authentification failed")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -771,7 +772,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
msg, err := h.database.GetMessage(ctx, models.MessageID(*messageCompNew), false)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(301, "Message not found")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -863,7 +864,7 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(201, "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -871,7 +872,7 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
|
|||
}
|
||||
|
||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.CompatAPIError(204, "Authentification failed")
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
|
||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ExternalHandler struct {
|
||||
app *logic.Application
|
||||
database *primarydb.Database
|
||||
}
|
||||
|
||||
func NewExternalHandler(app *logic.Application) ExternalHandler {
|
||||
return ExternalHandler{
|
||||
app: app,
|
||||
database: app.Database.Primary,
|
||||
}
|
||||
}
|
||||
|
||||
// UptimeKuma swaggerdoc
|
||||
//
|
||||
// @Summary Send a new message
|
||||
// @Description All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required
|
||||
// @Tags External
|
||||
//
|
||||
// @Param query_data query handler.UptimeKuma.query false " "
|
||||
// @Param post_body body handler.UptimeKuma.body false " "
|
||||
//
|
||||
// @Success 200 {object} handler.UptimeKuma.response
|
||||
// @Failure 400 {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 /external/v1/uptime-kuma [POST]
|
||||
func (h ExternalHandler) UptimeKuma(g *gin.Context) ginresp.HTTPResponse {
|
||||
type query struct {
|
||||
UserID *models.UserID `form:"user_id" example:"7725"`
|
||||
KeyToken *string `form:"key" example:"P3TNH8mvv14fm"`
|
||||
Channel *string `form:"channel"`
|
||||
ChannelUp *string `form:"channel_up"`
|
||||
ChannelDown *string `form:"channel_down"`
|
||||
Priority *int `form:"priority"`
|
||||
PriorityUp *int `form:"priority_up"`
|
||||
PriorityDown *int `form:"priority_down"`
|
||||
SenderName *string `form:"senderName"`
|
||||
}
|
||||
type body struct {
|
||||
Heartbeat *struct {
|
||||
Time string `json:"time"`
|
||||
Status int `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
Timezone string `json:"timezone"`
|
||||
TimezoneOffset string `json:"timezoneOffset"`
|
||||
LocalDateTime string `json:"localDateTime"`
|
||||
} `json:"heartbeat"`
|
||||
Monitor *struct {
|
||||
Name string `json:"name"`
|
||||
Url *string `json:"url"`
|
||||
} `json:"monitor"`
|
||||
Msg *string `json:"msg"`
|
||||
}
|
||||
type response struct {
|
||||
MessageID models.MessageID `json:"message_id"`
|
||||
}
|
||||
|
||||
var b body
|
||||
var q query
|
||||
ctx, httpErr := h.app.StartRequest(g, nil, &q, &b, nil)
|
||||
if httpErr != nil {
|
||||
return *httpErr
|
||||
}
|
||||
defer ctx.Cancel()
|
||||
|
||||
if b.Heartbeat == nil {
|
||||
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'heartbeat' in request body", nil)
|
||||
}
|
||||
if b.Monitor == nil {
|
||||
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'monitor' in request body", nil)
|
||||
}
|
||||
if b.Msg == nil {
|
||||
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'msg' in request body", nil)
|
||||
}
|
||||
|
||||
title := langext.Conditional(b.Heartbeat.Status == 1, fmt.Sprintf("Monitor %v is back online", b.Monitor.Name), fmt.Sprintf("Monitor %v went down!", b.Monitor.Name))
|
||||
|
||||
content := b.Heartbeat.Msg
|
||||
|
||||
var timestamp *float64 = nil
|
||||
if tz, err := time.LoadLocation(b.Heartbeat.Timezone); err == nil {
|
||||
if ts, err := time.ParseInLocation("2006-01-02 15:04:05", b.Heartbeat.LocalDateTime, tz); err == nil {
|
||||
timestamp = langext.Ptr(float64(ts.Unix()))
|
||||
}
|
||||
}
|
||||
|
||||
var channel *string = nil
|
||||
if q.Channel != nil {
|
||||
channel = q.Channel
|
||||
}
|
||||
if q.ChannelUp != nil && b.Heartbeat.Status == 1 {
|
||||
channel = q.ChannelUp
|
||||
}
|
||||
if q.ChannelDown != nil && b.Heartbeat.Status != 1 {
|
||||
channel = q.ChannelDown
|
||||
}
|
||||
|
||||
var priority *int = nil
|
||||
if q.Priority != nil {
|
||||
priority = q.Priority
|
||||
}
|
||||
if q.PriorityUp != nil && b.Heartbeat.Status == 1 {
|
||||
priority = q.PriorityUp
|
||||
}
|
||||
if q.PriorityDown != nil && b.Heartbeat.Status != 1 {
|
||||
priority = q.PriorityDown
|
||||
}
|
||||
|
||||
okResp, errResp := h.app.SendMessage(g, ctx, q.UserID, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName)
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
}
|
||||
|
||||
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{
|
||||
MessageID: okResp.Message.MessageID,
|
||||
}))
|
||||
}
|
|
@ -2,22 +2,14 @@ package handler
|
|||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
|
||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SendMessageResponse struct {
|
||||
|
@ -95,7 +87,7 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
|
|||
// query has highest prio, then form, then json
|
||||
data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q)
|
||||
|
||||
okResp, errResp := h.sendMessageInternal(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName)
|
||||
okResp, errResp := h.app.SendMessage(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName)
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
} else {
|
||||
|
@ -113,203 +105,3 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
|
|||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContext, UserID *models.UserID, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginresp.HTTPResponse) {
|
||||
if Title != nil {
|
||||
Title = langext.Ptr(strings.TrimSpace(*Title))
|
||||
}
|
||||
if UserMessageID != nil {
|
||||
UserMessageID = langext.Ptr(strings.TrimSpace(*UserMessageID))
|
||||
}
|
||||
|
||||
if UserID == nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_UID, hl.USER_ID, "Missing parameter [[user_id]]", nil))
|
||||
}
|
||||
if Key == nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[key]]", nil))
|
||||
}
|
||||
if Title == nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TITLE, hl.TITLE, "Missing parameter [[title]]", nil))
|
||||
}
|
||||
if Priority != nil && (*Priority != 0 && *Priority != 1 && *Priority != 2) {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.INVALID_PRIO, hl.PRIORITY, "Invalid priority", nil))
|
||||
}
|
||||
if len(*Title) == 0 {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.NO_TITLE, hl.TITLE, "No title specified", nil))
|
||||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, *UserID)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", err))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err))
|
||||
}
|
||||
|
||||
channelDisplayName := user.DefaultChannel()
|
||||
channelInternalName := user.DefaultChannel()
|
||||
if Channel != nil {
|
||||
channelDisplayName = h.app.NormalizeChannelDisplayName(*Channel)
|
||||
channelInternalName = h.app.NormalizeChannelInternalName(*Channel)
|
||||
}
|
||||
|
||||
if len(*Title) > user.MaxTitleLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.TITLE_TOO_LONG, hl.TITLE, fmt.Sprintf("Title too long (max %d characters)", user.MaxTitleLength()), nil))
|
||||
}
|
||||
if Content != nil && len(*Content) > user.MaxContentLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, hl.CONTENT, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil))
|
||||
}
|
||||
if len(channelDisplayName) > user.MaxChannelNameLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil))
|
||||
}
|
||||
if len(strings.TrimSpace(channelDisplayName)) == 0 {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_NAME_EMPTY, hl.CHANNEL, fmt.Sprintf("Channel displayname cannot be empty"), nil))
|
||||
}
|
||||
if len(channelInternalName) > user.MaxChannelNameLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil))
|
||||
}
|
||||
if len(strings.TrimSpace(channelInternalName)) == 0 {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_NAME_EMPTY, hl.CHANNEL, fmt.Sprintf("Channel internalname cannot be empty"), nil))
|
||||
}
|
||||
if SenderName != nil && len(*SenderName) > user.MaxSenderName() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.SENDERNAME_TOO_LONG, hl.SENDER_NAME, fmt.Sprintf("SenderName too long (max %d characters)", user.MaxSenderName()), nil))
|
||||
}
|
||||
if UserMessageID != nil && len(*UserMessageID) > user.MaxUserMessageID() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.USR_MSG_ID_TOO_LONG, hl.USER_MESSAGE_ID, fmt.Sprintf("MessageID too long (max %d characters)", user.MaxUserMessageID()), nil))
|
||||
}
|
||||
if SendTimestamp != nil && mathext.Abs(*SendTimestamp-float64(time.Now().Unix())) > timeext.FromHours(user.MaxTimestampDiffHours()).Seconds() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.TIMESTAMP_OUT_OF_RANGE, hl.NONE, fmt.Sprintf("The timestamp mus be within %d hours of now()", user.MaxTimestampDiffHours()), nil))
|
||||
}
|
||||
|
||||
if UserMessageID != nil {
|
||||
msg, err := h.database.GetMessageByUserMessageID(ctx, *UserMessageID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query existing message", err))
|
||||
}
|
||||
if msg != nil {
|
||||
|
||||
existingCompID, _, err := h.database.ConvertToCompatID(ctx, msg.MessageID.String())
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query compat-id", err))
|
||||
}
|
||||
|
||||
if existingCompID == nil {
|
||||
v, err := h.database.CreateCompatID(ctx, "messageid", msg.MessageID.String())
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
|
||||
}
|
||||
existingCompID = &v
|
||||
}
|
||||
|
||||
//the found message can be deleted (!), but we still return NO_ERROR here...
|
||||
return &SendMessageResponse{
|
||||
User: user,
|
||||
Message: *msg,
|
||||
MessageIsOld: true,
|
||||
CompatMessageID: *existingCompID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
if user.QuotaRemainingToday() <= 0 {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, hl.NONE, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil))
|
||||
}
|
||||
|
||||
channel, err := h.app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err))
|
||||
}
|
||||
|
||||
keytok, permResp := ctx.CheckPermissionSend(channel, *Key)
|
||||
if permResp != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, hl.USER_KEY, "You are not authorized for this action", nil))
|
||||
}
|
||||
|
||||
var sendTimestamp *time.Time = nil
|
||||
if SendTimestamp != nil {
|
||||
sendTimestamp = langext.Ptr(timeext.UnixFloatSeconds(*SendTimestamp))
|
||||
}
|
||||
|
||||
priority := langext.Coalesce(Priority, user.DefaultPriority())
|
||||
|
||||
clientIP := g.ClientIP()
|
||||
|
||||
msg, err := h.database.CreateMessage(ctx, *UserID, channel, sendTimestamp, *Title, Content, priority, UserMessageID, clientIP, SenderName, keytok.KeyTokenID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create message in db", err))
|
||||
}
|
||||
|
||||
compatMsgID, err := h.database.CreateCompatID(ctx, "messageid", msg.MessageID.String())
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
|
||||
}
|
||||
|
||||
subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query subscriptions", err))
|
||||
}
|
||||
|
||||
err = h.database.IncUserMessageCounter(ctx, &user)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc user msg-counter", err))
|
||||
}
|
||||
|
||||
err = h.database.IncChannelMessageCounter(ctx, &channel)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc channel msg-counter", err))
|
||||
}
|
||||
|
||||
err = h.database.IncKeyTokenMessageCounter(ctx, keytok)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc token msg-counter", err))
|
||||
}
|
||||
|
||||
log.Info().Msg(fmt.Sprintf("Sending new notification %s for user %s", msg.MessageID, UserID))
|
||||
|
||||
for _, sub := range subscriptions {
|
||||
clients, err := h.database.ListClients(ctx, sub.SubscriberUserID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query clients", err))
|
||||
}
|
||||
|
||||
if !sub.Confirmed {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
|
||||
isCompatClient, err := h.database.IsCompatClient(ctx, client.ClientID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query compat_clients", err))
|
||||
}
|
||||
|
||||
var titleOverride *string = nil
|
||||
var msgidOverride *string = nil
|
||||
if isCompatClient {
|
||||
titleOverride = langext.Ptr(h.app.CompatizeMessageTitle(ctx, msg))
|
||||
msgidOverride = langext.Ptr(fmt.Sprintf("%d", compatMsgID))
|
||||
}
|
||||
|
||||
fcmDelivID, err := h.app.DeliverMessage(ctx, client, msg, titleOverride, msgidOverride)
|
||||
if err != nil {
|
||||
_, err = h.database.CreateRetryDelivery(ctx, client, msg)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create delivery", err))
|
||||
}
|
||||
} else {
|
||||
_, err = h.database.CreateSuccessDelivery(ctx, client, msg, fcmDelivID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create delivery", err))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return &SendMessageResponse{
|
||||
User: user,
|
||||
Message: msg,
|
||||
MessageIsOld: false,
|
||||
CompatMessageID: compatMsgID,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -16,22 +16,24 @@ import (
|
|||
type Router struct {
|
||||
app *logic.Application
|
||||
|
||||
commonHandler handler.CommonHandler
|
||||
compatHandler handler.CompatHandler
|
||||
websiteHandler handler.WebsiteHandler
|
||||
apiHandler handler.APIHandler
|
||||
messageHandler handler.MessageHandler
|
||||
commonHandler handler.CommonHandler
|
||||
compatHandler handler.CompatHandler
|
||||
websiteHandler handler.WebsiteHandler
|
||||
apiHandler handler.APIHandler
|
||||
messageHandler handler.MessageHandler
|
||||
externalHandler handler.ExternalHandler
|
||||
}
|
||||
|
||||
func NewRouter(app *logic.Application) *Router {
|
||||
return &Router{
|
||||
app: app,
|
||||
|
||||
commonHandler: handler.NewCommonHandler(app),
|
||||
compatHandler: handler.NewCompatHandler(app),
|
||||
websiteHandler: handler.NewWebsiteHandler(app),
|
||||
apiHandler: handler.NewAPIHandler(app),
|
||||
messageHandler: handler.NewMessageHandler(app),
|
||||
commonHandler: handler.NewCommonHandler(app),
|
||||
compatHandler: handler.NewCompatHandler(app),
|
||||
websiteHandler: handler.NewWebsiteHandler(app),
|
||||
apiHandler: handler.NewAPIHandler(app),
|
||||
messageHandler: handler.NewMessageHandler(app),
|
||||
externalHandler: handler.NewExternalHandler(app),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +124,6 @@ func (r *Router) Init(e *gin.Engine) error {
|
|||
|
||||
apiv2 := e.Group("/api/v2/")
|
||||
{
|
||||
|
||||
apiv2.POST("/users", r.Wrap(r.apiHandler.CreateUser))
|
||||
apiv2.GET("/users/:uid", r.Wrap(r.apiHandler.GetUser))
|
||||
apiv2.PATCH("/users/:uid", r.Wrap(r.apiHandler.UpdateUser))
|
||||
|
@ -163,7 +164,10 @@ func (r *Router) Init(e *gin.Engine) error {
|
|||
{
|
||||
sendAPI.POST("/", r.Wrap(r.messageHandler.SendMessage))
|
||||
sendAPI.POST("/send", r.Wrap(r.messageHandler.SendMessage))
|
||||
sendAPI.POST("/send.php", r.Wrap(r.messageHandler.SendMessageCompat))
|
||||
sendAPI.POST("/send.php", r.Wrap(r.compatHandler.SendMessage))
|
||||
|
||||
sendAPI.POST("/external/v1/uptime-kuma", r.Wrap(r.externalHandler.UptimeKuma))
|
||||
|
||||
}
|
||||
|
||||
// ================
|
||||
|
|
|
@ -36,6 +36,13 @@ func main() {
|
|||
}
|
||||
fmt.Printf("PrimarySchema3 := %s\n", h0)
|
||||
}
|
||||
{
|
||||
h0, err := sq.HashSqliteSchema(ctx, schema.PrimarySchema4)
|
||||
if err != nil {
|
||||
h0 = "ERR"
|
||||
}
|
||||
fmt.Printf("PrimarySchema4 := %s\n", h0)
|
||||
}
|
||||
{
|
||||
h0, err := sq.HashSqliteSchema(ctx, schema.RequestsSchema1)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package primary
|
||||
package db
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
"time"
|
||||
)
|
||||
|
@ -12,5 +11,5 @@ type TxContext interface {
|
|||
Err() error
|
||||
Value(key any) any
|
||||
|
||||
GetOrCreateTransaction(db db.DatabaseImpl) (sq.Tx, error)
|
||||
GetOrCreateTransaction(db DatabaseImpl) (sq.Tx, error)
|
||||
}
|
|
@ -13,17 +13,17 @@ type DatabaseImpl interface {
|
|||
BeginTx(ctx context.Context) (sq.Tx, error)
|
||||
Stop(ctx context.Context) error
|
||||
|
||||
ReadSchema(ctx context.Context) (int, error)
|
||||
ReadSchema(ctx TxContext) (int, error)
|
||||
|
||||
WriteMetaString(ctx context.Context, key string, value string) error
|
||||
WriteMetaInt(ctx context.Context, key string, value int64) error
|
||||
WriteMetaReal(ctx context.Context, key string, value float64) error
|
||||
WriteMetaBlob(ctx context.Context, key string, value []byte) error
|
||||
WriteMetaString(ctx TxContext, key string, value string) error
|
||||
WriteMetaInt(ctx TxContext, key string, value int64) error
|
||||
WriteMetaReal(ctx TxContext, key string, value float64) error
|
||||
WriteMetaBlob(ctx TxContext, key string, value []byte) error
|
||||
|
||||
ReadMetaString(ctx context.Context, key string) (*string, error)
|
||||
ReadMetaInt(ctx context.Context, key string) (*int64, error)
|
||||
ReadMetaReal(ctx context.Context, key string) (*float64, error)
|
||||
ReadMetaBlob(ctx context.Context, key string) (*[]byte, error)
|
||||
ReadMetaString(ctx TxContext, key string) (*string, error)
|
||||
ReadMetaInt(ctx TxContext, key string) (*int64, error)
|
||||
ReadMetaReal(ctx TxContext, key string) (*float64, error)
|
||||
ReadMetaBlob(ctx TxContext, key string) (*[]byte, error)
|
||||
|
||||
DeleteMeta(ctx context.Context, key string) error
|
||||
DeleteMeta(ctx TxContext, key string) error
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
server "blackforestbytes.com/simplecloudnotifier"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/dbtools"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/schema"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
@ -63,77 +64,93 @@ func (db *Database) DB() sq.DB {
|
|||
return db.db
|
||||
}
|
||||
|
||||
func (db *Database) Migrate(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second)
|
||||
defer cancel()
|
||||
func (db *Database) Migrate(outerctx context.Context) error {
|
||||
innerctx, cancel := context.WithTimeout(outerctx, 24*time.Second)
|
||||
tctx := simplectx.CreateSimpleContext(innerctx, cancel)
|
||||
|
||||
currschema, err := db.ReadSchema(ctx)
|
||||
tx, err := tctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if tx.Status() == sq.TxStatusInitial || tx.Status() == sq.TxStatusActive {
|
||||
err = tx.Rollback()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to rollback transaction")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ppReInit := false
|
||||
|
||||
currschema, err := db.ReadSchema(tctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if currschema == 0 {
|
||||
schemastr := schema.LogsSchema[schema.LogsSchemaVersion].SQL
|
||||
schemahash := schema.LogsSchema[schema.LogsSchemaVersion].Hash
|
||||
|
||||
schemastr := schema.LogsSchema1
|
||||
|
||||
schemahash, err := sq.HashSqliteSchema(ctx, schemastr)
|
||||
_, err = tx.Exec(tctx, schemastr, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.db.Exec(ctx, schemastr, sq.PP{})
|
||||
err = db.WriteMetaInt(tctx, "schema", int64(schema.LogsSchemaVersion))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WriteMetaInt(ctx, "schema", 1)
|
||||
err = db.WriteMetaString(tctx, "schema_hash", schemahash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WriteMetaString(ctx, "schema_hash", schemahash)
|
||||
ppReInit = true
|
||||
|
||||
currschema = schema.LogsSchemaVersion
|
||||
}
|
||||
|
||||
if currschema == 1 {
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.pp.Init(ctx) // Re-Init
|
||||
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
} else if currschema == 1 {
|
||||
|
||||
schemHashDB, err := sq.HashSqliteDatabase(ctx, db.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemaHashMeta, err := db.ReadMetaString(ctx, "schema_hash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemHashAsset := schema.LogsHash1
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schemHashAsset {
|
||||
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.LogsSchema[currschema].Hash {
|
||||
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (logs db)")
|
||||
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).Msg("Schema (logs db)")
|
||||
log.Debug().Str("schemHashAsset", schemHashAsset).Msg("Schema (logs db)")
|
||||
log.Debug().Str("schemaHashAsset", schema.LogsSchema[currschema].Hash).Msg("Schema (logs db)")
|
||||
return errors.New("database schema does not match (logs db)")
|
||||
} else {
|
||||
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (logs db)")
|
||||
}
|
||||
}
|
||||
|
||||
return nil // current
|
||||
} else {
|
||||
if currschema != schema.LogsSchemaVersion {
|
||||
return errors.New(fmt.Sprintf("Unknown DB schema: %d", currschema))
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ppReInit {
|
||||
log.Debug().Msg("Re-Init preprocessor")
|
||||
err = db.pp.Init(outerctx) // Re-Init
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) Ping(ctx context.Context) error {
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
package logs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"errors"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
)
|
||||
|
||||
func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
|
||||
func (db *Database) ReadSchema(ctx db.TxContext) (retval int, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
r1, err := db.db.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
|
||||
r1, err := tx.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -31,7 +35,7 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
|
|||
return 0, err
|
||||
}
|
||||
|
||||
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
|
||||
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -62,8 +66,13 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
|
|||
return dbschema, nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
func (db *Database) WriteMetaString(ctx db.TxContext, key string, value string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -73,8 +82,13 @@ func (db *Database) WriteMetaString(ctx context.Context, key string, value strin
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
func (db *Database) WriteMetaInt(ctx db.TxContext, key string, value int64) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -84,8 +98,13 @@ func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
|
||||
func (db *Database) WriteMetaReal(ctx db.TxContext, key string, value float64) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -95,8 +114,13 @@ func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
|
||||
func (db *Database) WriteMetaBlob(ctx db.TxContext, key string, value []byte) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -106,8 +130,13 @@ func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *string, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaString(ctx db.TxContext, key string) (retval *string, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -137,8 +166,13 @@ func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *str
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaInt(ctx db.TxContext, key string) (retval *int64, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -169,8 +203,13 @@ func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64,
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float64, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaReal(ctx db.TxContext, key string) (retval *float64, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -201,8 +240,13 @@ func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byte, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaBlob(ctx db.TxContext, key string) (retval *[]byte, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -233,8 +277,13 @@ func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byt
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteMeta(ctx context.Context, key string) error {
|
||||
_, err := db.db.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) DeleteMeta(ctx db.TxContext, key string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package primary
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanName string) (*models.Channel, error) {
|
||||
func (db *Database) GetChannelByName(ctx db.TxContext, userid models.UserID, chanName string) (*models.Channel, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -22,7 +24,7 @@ func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanNa
|
|||
}
|
||||
|
||||
channel, err := models.DecodeChannel(rows)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -32,7 +34,7 @@ func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanNa
|
|||
return &channel, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*models.Channel, error) {
|
||||
func (db *Database) GetChannelByID(ctx db.TxContext, chanid models.ChannelID) (*models.Channel, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -46,7 +48,7 @@ func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*mod
|
|||
}
|
||||
|
||||
channel, err := models.DecodeChannel(rows)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -56,7 +58,15 @@ func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*mod
|
|||
return &channel, nil
|
||||
}
|
||||
|
||||
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string) (models.Channel, error) {
|
||||
type CreateChanel struct {
|
||||
UserId models.UserID
|
||||
DisplayName string
|
||||
IntName string
|
||||
SubscribeKey string
|
||||
Description *string
|
||||
}
|
||||
|
||||
func (db *Database) CreateChannel(ctx db.TxContext, channel CreateChanel) (models.Channel, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.Channel{}, err
|
||||
|
@ -64,10 +74,11 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName
|
|||
|
||||
entity := models.ChannelDB{
|
||||
ChannelID: models.NewChannelID(),
|
||||
OwnerUserID: userid,
|
||||
DisplayName: dispName,
|
||||
InternalName: intName,
|
||||
SubscribeKey: subscribeKey,
|
||||
OwnerUserID: channel.UserId,
|
||||
DisplayName: channel.DisplayName,
|
||||
InternalName: channel.IntName,
|
||||
SubscribeKey: channel.SubscribeKey,
|
||||
DescriptionName: channel.Description,
|
||||
TimestampCreated: time2DB(time.Now()),
|
||||
TimestampLastSent: nil,
|
||||
MessagesSent: 0,
|
||||
|
@ -81,7 +92,7 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName
|
|||
return entity.Model(), nil
|
||||
}
|
||||
|
||||
func (db *Database) ListChannelsByOwner(ctx TxContext, userid models.UserID, subUserID models.UserID) ([]models.ChannelWithSubscription, error) {
|
||||
func (db *Database) ListChannelsByOwner(ctx db.TxContext, userid models.UserID, subUserID models.UserID) ([]models.ChannelWithSubscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -105,7 +116,7 @@ func (db *Database) ListChannelsByOwner(ctx TxContext, userid models.UserID, sub
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (db *Database) ListChannelsBySubscriber(ctx TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) {
|
||||
func (db *Database) ListChannelsBySubscriber(ctx db.TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -135,7 +146,7 @@ func (db *Database) ListChannelsBySubscriber(ctx TxContext, userid models.UserID
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (db *Database) ListChannelsByAccess(ctx TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) {
|
||||
func (db *Database) ListChannelsByAccess(ctx db.TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -166,7 +177,7 @@ func (db *Database) ListChannelsByAccess(ctx TxContext, userid models.UserID, co
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetChannel(ctx TxContext, userid models.UserID, channelid models.ChannelID, enforceOwner bool) (models.ChannelWithSubscription, error) {
|
||||
func (db *Database) GetChannel(ctx db.TxContext, userid models.UserID, channelid models.ChannelID, enforceOwner bool) (models.ChannelWithSubscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.ChannelWithSubscription{}, err
|
||||
|
@ -200,7 +211,7 @@ func (db *Database) GetChannel(ctx TxContext, userid models.UserID, channelid mo
|
|||
return channel, nil
|
||||
}
|
||||
|
||||
func (db *Database) IncChannelMessageCounter(ctx TxContext, channel *models.Channel) error {
|
||||
func (db *Database) IncChannelMessageCounter(ctx db.TxContext, channel *models.Channel) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -222,7 +233,7 @@ func (db *Database) IncChannelMessageCounter(ctx TxContext, channel *models.Chan
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateChannelSubscribeKey(ctx TxContext, channelid models.ChannelID, newkey string) error {
|
||||
func (db *Database) UpdateChannelSubscribeKey(ctx db.TxContext, channelid models.ChannelID, newkey string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -239,7 +250,7 @@ func (db *Database) UpdateChannelSubscribeKey(ctx TxContext, channelid models.Ch
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateChannelDisplayName(ctx TxContext, channelid models.ChannelID, dispname string) error {
|
||||
func (db *Database) UpdateChannelDisplayName(ctx db.TxContext, channelid models.ChannelID, dispname string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -256,7 +267,7 @@ func (db *Database) UpdateChannelDisplayName(ctx TxContext, channelid models.Cha
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateChannelDescriptionName(ctx TxContext, channelid models.ChannelID, descname *string) error {
|
||||
func (db *Database) UpdateChannelDescriptionName(ctx db.TxContext, channelid models.ChannelID, descname *string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package primary
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (db *Database) CreateClient(ctx TxContext, userid models.UserID, ctype models.ClientType, fcmToken string, agentModel string, agentVersion string) (models.Client, error) {
|
||||
func (db *Database) CreateClient(ctx db.TxContext, userid models.UserID, ctype models.ClientType, fcmToken string, agentModel string, agentVersion string) (models.Client, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.Client{}, err
|
||||
|
@ -30,7 +31,7 @@ func (db *Database) CreateClient(ctx TxContext, userid models.UserID, ctype mode
|
|||
return entity.Model(), nil
|
||||
}
|
||||
|
||||
func (db *Database) ClearFCMTokens(ctx TxContext, fcmtoken string) error {
|
||||
func (db *Database) ClearFCMTokens(ctx db.TxContext, fcmtoken string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -44,7 +45,7 @@ func (db *Database) ClearFCMTokens(ctx TxContext, fcmtoken string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) ListClients(ctx TxContext, userid models.UserID) ([]models.Client, error) {
|
||||
func (db *Database) ListClients(ctx db.TxContext, userid models.UserID) ([]models.Client, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -63,7 +64,7 @@ func (db *Database) ListClients(ctx TxContext, userid models.UserID) ([]models.C
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetClient(ctx TxContext, userid models.UserID, clientid models.ClientID) (models.Client, error) {
|
||||
func (db *Database) GetClient(ctx db.TxContext, userid models.UserID, clientid models.ClientID) (models.Client, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.Client{}, err
|
||||
|
@ -85,7 +86,7 @@ func (db *Database) GetClient(ctx TxContext, userid models.UserID, clientid mode
|
|||
return client, nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteClient(ctx TxContext, clientid models.ClientID) error {
|
||||
func (db *Database) DeleteClient(ctx db.TxContext, clientid models.ClientID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -99,7 +100,7 @@ func (db *Database) DeleteClient(ctx TxContext, clientid models.ClientID) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteClientsByFCM(ctx TxContext, fcmtoken string) error {
|
||||
func (db *Database) DeleteClientsByFCM(ctx db.TxContext, fcmtoken string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -113,7 +114,7 @@ func (db *Database) DeleteClientsByFCM(ctx TxContext, fcmtoken string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateClientFCMToken(ctx TxContext, clientid models.ClientID, fcmtoken string) error {
|
||||
func (db *Database) UpdateClientFCMToken(ctx db.TxContext, clientid models.ClientID, fcmtoken string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -130,7 +131,7 @@ func (db *Database) UpdateClientFCMToken(ctx TxContext, clientid models.ClientID
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateClientAgentModel(ctx TxContext, clientid models.ClientID, agentModel string) error {
|
||||
func (db *Database) UpdateClientAgentModel(ctx db.TxContext, clientid models.ClientID, agentModel string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -147,7 +148,7 @@ func (db *Database) UpdateClientAgentModel(ctx TxContext, clientid models.Client
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateClientAgentVersion(ctx TxContext, clientid models.ClientID, agentVersion string) error {
|
||||
func (db *Database) UpdateClientAgentVersion(ctx db.TxContext, clientid models.ClientID, agentVersion string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package primary
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
)
|
||||
|
||||
func (db *Database) CreateCompatID(ctx TxContext, idtype string, newid string) (int64, error) {
|
||||
func (db *Database) CreateCompatID(ctx db.TxContext, idtype string, newid string) (int64, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -42,7 +43,7 @@ func (db *Database) CreateCompatID(ctx TxContext, idtype string, newid string) (
|
|||
return oldid, nil
|
||||
}
|
||||
|
||||
func (db *Database) ConvertCompatID(ctx TxContext, oldid int64, idtype string) (*string, error) {
|
||||
func (db *Database) ConvertCompatID(ctx db.TxContext, oldid int64, idtype string) (*string, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -62,7 +63,7 @@ func (db *Database) ConvertCompatID(ctx TxContext, oldid int64, idtype string) (
|
|||
|
||||
var newid string
|
||||
err = rows.Scan(&newid)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -72,7 +73,7 @@ func (db *Database) ConvertCompatID(ctx TxContext, oldid int64, idtype string) (
|
|||
return &newid, nil
|
||||
}
|
||||
|
||||
func (db *Database) ConvertToCompatID(ctx TxContext, newid string) (*int64, *string, error) {
|
||||
func (db *Database) ConvertToCompatID(ctx db.TxContext, newid string) (*int64, *string, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -90,7 +91,7 @@ func (db *Database) ConvertToCompatID(ctx TxContext, newid string) (*int64, *str
|
|||
var oldid int64
|
||||
var idtype string
|
||||
err = rows.Scan(&oldid, &idtype)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -100,7 +101,7 @@ func (db *Database) ConvertToCompatID(ctx TxContext, newid string) (*int64, *str
|
|||
return &oldid, &idtype, nil
|
||||
}
|
||||
|
||||
func (db *Database) ConvertToCompatIDOrCreate(ctx TxContext, idtype string, newid string) (int64, error) {
|
||||
func (db *Database) ConvertToCompatIDOrCreate(ctx db.TxContext, idtype string, newid string) (int64, error) {
|
||||
id1, _, err := db.ConvertToCompatID(ctx, newid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -116,7 +117,7 @@ func (db *Database) ConvertToCompatIDOrCreate(ctx TxContext, idtype string, newi
|
|||
return id2, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetAck(ctx TxContext, msgid models.MessageID) (bool, error) {
|
||||
func (db *Database) GetAck(ctx db.TxContext, msgid models.MessageID) (bool, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -139,7 +140,7 @@ func (db *Database) GetAck(ctx TxContext, msgid models.MessageID) (bool, error)
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (db *Database) SetAck(ctx TxContext, userid models.UserID, msgid models.MessageID) error {
|
||||
func (db *Database) SetAck(ctx db.TxContext, userid models.UserID, msgid models.MessageID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -156,7 +157,7 @@ func (db *Database) SetAck(ctx TxContext, userid models.UserID, msgid models.Mes
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) IsCompatClient(ctx TxContext, clientid models.ClientID) (bool, error) {
|
||||
func (db *Database) IsCompatClient(ctx db.TxContext, clientid models.ClientID) (bool, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
server "blackforestbytes.com/simplecloudnotifier"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/dbtools"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/schema"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
@ -63,81 +64,147 @@ func (db *Database) DB() sq.DB {
|
|||
return db.db
|
||||
}
|
||||
|
||||
func (db *Database) Migrate(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second)
|
||||
defer cancel()
|
||||
func (db *Database) Migrate(outerctx context.Context) error {
|
||||
innerctx, cancel := context.WithTimeout(outerctx, 24*time.Second)
|
||||
tctx := simplectx.CreateSimpleContext(innerctx, cancel)
|
||||
|
||||
currschema, err := db.ReadSchema(ctx)
|
||||
tx, err := tctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if tx.Status() == sq.TxStatusInitial || tx.Status() == sq.TxStatusActive {
|
||||
err = tx.Rollback()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to rollback transaction")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ppReInit := false
|
||||
|
||||
currschema, err := db.ReadSchema(tctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if currschema == 0 {
|
||||
schemastr := schema.PrimarySchema[schema.PrimarySchemaVersion].SQL
|
||||
schemahash := schema.PrimarySchema[schema.PrimarySchemaVersion].Hash
|
||||
|
||||
schemastr := schema.PrimarySchema3
|
||||
|
||||
schemahash, err := sq.HashSqliteSchema(ctx, schemastr)
|
||||
_, err = tx.Exec(tctx, schemastr, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.db.Exec(ctx, schemastr, sq.PP{})
|
||||
err = db.WriteMetaInt(tctx, "schema", int64(schema.PrimarySchemaVersion))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WriteMetaInt(ctx, "schema", 3)
|
||||
err = db.WriteMetaString(tctx, "schema_hash", schemahash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WriteMetaString(ctx, "schema_hash", schemahash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ppReInit = true
|
||||
|
||||
err = db.pp.Init(ctx) // Re-Init
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currschema = schema.PrimarySchemaVersion
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
} else if currschema == 1 {
|
||||
if currschema == 1 {
|
||||
return errors.New("cannot autom. upgrade schema 1")
|
||||
} else if currschema == 2 {
|
||||
}
|
||||
|
||||
if currschema == 2 {
|
||||
return errors.New("cannot autom. upgrade schema 2")
|
||||
} else if currschema == 3 {
|
||||
}
|
||||
|
||||
schemHashDB, err := sq.HashSqliteDatabase(ctx, db.db)
|
||||
if currschema == 3 {
|
||||
|
||||
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemaHashMeta, err := db.ReadMetaString(ctx, "schema_hash")
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemHashAsset := schema.PrimaryHash3
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schemHashAsset {
|
||||
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.PrimarySchema[currschema].Hash {
|
||||
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (primary db)")
|
||||
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).Msg("Schema (primary db)")
|
||||
log.Debug().Str("schemHashAsset", schemHashAsset).Msg("Schema (primary db)")
|
||||
log.Debug().Str("schemaHashAsset", schema.PrimarySchema[currschema].Hash).Msg("Schema (primary db)")
|
||||
return errors.New("database schema does not match (primary db)")
|
||||
} else {
|
||||
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (primary db)")
|
||||
}
|
||||
|
||||
return nil // current
|
||||
} else {
|
||||
log.Info().Int("currschema", currschema).Msg("Upgrade schema from 3 -> 4")
|
||||
|
||||
_, err = tx.Exec(tctx, schema.PrimaryMigration_3_4, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currschema = 4
|
||||
|
||||
err = db.WriteMetaInt(tctx, "schema", int64(currschema))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WriteMetaString(tctx, "schema_hash", schema.PrimarySchema[currschema].Hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Int("currschema", currschema).Msg("Upgrade schema from 3 -> 4 succesfuly")
|
||||
|
||||
ppReInit = true
|
||||
}
|
||||
|
||||
if currschema == 4 {
|
||||
|
||||
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.PrimarySchema[currschema].Hash {
|
||||
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (primary db)")
|
||||
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).Msg("Schema (primary db)")
|
||||
log.Debug().Str("schemaHashAsset", schema.PrimarySchema[currschema].Hash).Msg("Schema (primary db)")
|
||||
return errors.New("database schema does not match (primary db)")
|
||||
} else {
|
||||
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (primary db)")
|
||||
}
|
||||
}
|
||||
|
||||
if currschema != schema.PrimarySchemaVersion {
|
||||
return errors.New(fmt.Sprintf("Unknown DB schema: %d", currschema))
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ppReInit {
|
||||
log.Debug().Msg("Re-Init preprocessor")
|
||||
err = db.pp.Init(outerctx) // Re-Init
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) Ping(ctx context.Context) error {
|
||||
|
|
|
@ -2,13 +2,14 @@ package primary
|
|||
|
||||
import (
|
||||
scn "blackforestbytes.com/simplecloudnotifier"
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (db *Database) CreateRetryDelivery(ctx TxContext, client models.Client, msg models.Message) (models.Delivery, error) {
|
||||
func (db *Database) CreateRetryDelivery(ctx db.TxContext, client models.Client, msg models.Message) (models.Delivery, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.Delivery{}, err
|
||||
|
@ -38,7 +39,7 @@ func (db *Database) CreateRetryDelivery(ctx TxContext, client models.Client, msg
|
|||
return entity.Model(), nil
|
||||
}
|
||||
|
||||
func (db *Database) CreateSuccessDelivery(ctx TxContext, client models.Client, msg models.Message, fcmDelivID string) (models.Delivery, error) {
|
||||
func (db *Database) CreateSuccessDelivery(ctx db.TxContext, client models.Client, msg models.Message, fcmDelivID string) (models.Delivery, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.Delivery{}, err
|
||||
|
@ -67,7 +68,7 @@ func (db *Database) CreateSuccessDelivery(ctx TxContext, client models.Client, m
|
|||
return entity.Model(), nil
|
||||
}
|
||||
|
||||
func (db *Database) ListRetrieableDeliveries(ctx TxContext, pageSize int) ([]models.Delivery, error) {
|
||||
func (db *Database) ListRetrieableDeliveries(ctx db.TxContext, pageSize int) ([]models.Delivery, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -89,7 +90,7 @@ func (db *Database) ListRetrieableDeliveries(ctx TxContext, pageSize int) ([]mod
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (db *Database) SetDeliverySuccess(ctx TxContext, delivery models.Delivery, fcmDelivID string) error {
|
||||
func (db *Database) SetDeliverySuccess(ctx db.TxContext, delivery models.Delivery, fcmDelivID string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -108,7 +109,7 @@ func (db *Database) SetDeliverySuccess(ctx TxContext, delivery models.Delivery,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) SetDeliveryFailed(ctx TxContext, delivery models.Delivery) error {
|
||||
func (db *Database) SetDeliveryFailed(ctx db.TxContext, delivery models.Delivery) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -127,7 +128,7 @@ func (db *Database) SetDeliveryFailed(ctx TxContext, delivery models.Delivery) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) SetDeliveryRetry(ctx TxContext, delivery models.Delivery) error {
|
||||
func (db *Database) SetDeliveryRetry(ctx db.TxContext, delivery models.Delivery) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -145,7 +146,7 @@ func (db *Database) SetDeliveryRetry(ctx TxContext, delivery models.Delivery) er
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) CancelPendingDeliveries(ctx TxContext, messageID models.MessageID) error {
|
||||
func (db *Database) CancelPendingDeliveries(ctx db.TxContext, messageID models.MessageID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
package primary
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (db *Database) CreateKeyToken(ctx TxContext, name string, owner models.UserID, allChannels bool, channels []models.ChannelID, permissions models.TokenPermissionList, token string) (models.KeyToken, error) {
|
||||
func (db *Database) CreateKeyToken(ctx db.TxContext, name string, owner models.UserID, allChannels bool, channels []models.ChannelID, permissions models.TokenPermissionList, token string) (models.KeyToken, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.KeyToken{}, err
|
||||
|
@ -36,7 +38,7 @@ func (db *Database) CreateKeyToken(ctx TxContext, name string, owner models.User
|
|||
return entity.Model(), nil
|
||||
}
|
||||
|
||||
func (db *Database) ListKeyTokens(ctx TxContext, ownerID models.UserID) ([]models.KeyToken, error) {
|
||||
func (db *Database) ListKeyTokens(ctx db.TxContext, ownerID models.UserID) ([]models.KeyToken, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -55,7 +57,7 @@ func (db *Database) ListKeyTokens(ctx TxContext, ownerID models.UserID) ([]model
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetKeyToken(ctx TxContext, userid models.UserID, keyTokenid models.KeyTokenID) (models.KeyToken, error) {
|
||||
func (db *Database) GetKeyToken(ctx db.TxContext, userid models.UserID, keyTokenid models.KeyTokenID) (models.KeyToken, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.KeyToken{}, err
|
||||
|
@ -77,7 +79,7 @@ func (db *Database) GetKeyToken(ctx TxContext, userid models.UserID, keyTokenid
|
|||
return keyToken, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetKeyTokenByToken(ctx TxContext, key string) (*models.KeyToken, error) {
|
||||
func (db *Database) GetKeyTokenByToken(ctx db.TxContext, key string) (*models.KeyToken, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -89,7 +91,7 @@ func (db *Database) GetKeyTokenByToken(ctx TxContext, key string) (*models.KeyTo
|
|||
}
|
||||
|
||||
user, err := models.DecodeKeyToken(rows)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -99,7 +101,7 @@ func (db *Database) GetKeyTokenByToken(ctx TxContext, key string) (*models.KeyTo
|
|||
return &user, nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteKeyToken(ctx TxContext, keyTokenid models.KeyTokenID) error {
|
||||
func (db *Database) DeleteKeyToken(ctx db.TxContext, keyTokenid models.KeyTokenID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -113,7 +115,7 @@ func (db *Database) DeleteKeyToken(ctx TxContext, keyTokenid models.KeyTokenID)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateKeyTokenName(ctx TxContext, keyTokenid models.KeyTokenID, name string) error {
|
||||
func (db *Database) UpdateKeyTokenName(ctx db.TxContext, keyTokenid models.KeyTokenID, name string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -130,7 +132,7 @@ func (db *Database) UpdateKeyTokenName(ctx TxContext, keyTokenid models.KeyToken
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateKeyTokenPermissions(ctx TxContext, keyTokenid models.KeyTokenID, perm models.TokenPermissionList) error {
|
||||
func (db *Database) UpdateKeyTokenPermissions(ctx db.TxContext, keyTokenid models.KeyTokenID, perm models.TokenPermissionList) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -147,7 +149,7 @@ func (db *Database) UpdateKeyTokenPermissions(ctx TxContext, keyTokenid models.K
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateKeyTokenAllChannels(ctx TxContext, keyTokenid models.KeyTokenID, allChannels bool) error {
|
||||
func (db *Database) UpdateKeyTokenAllChannels(ctx db.TxContext, keyTokenid models.KeyTokenID, allChannels bool) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -164,7 +166,7 @@ func (db *Database) UpdateKeyTokenAllChannels(ctx TxContext, keyTokenid models.K
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateKeyTokenChannels(ctx TxContext, keyTokenid models.KeyTokenID, channels []models.ChannelID) error {
|
||||
func (db *Database) UpdateKeyTokenChannels(ctx db.TxContext, keyTokenid models.KeyTokenID, channels []models.ChannelID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -181,7 +183,7 @@ func (db *Database) UpdateKeyTokenChannels(ctx TxContext, keyTokenid models.KeyT
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) IncKeyTokenMessageCounter(ctx TxContext, keyToken *models.KeyToken) error {
|
||||
func (db *Database) IncKeyTokenMessageCounter(ctx db.TxContext, keyToken *models.KeyToken) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -203,7 +205,7 @@ func (db *Database) IncKeyTokenMessageCounter(ctx TxContext, keyToken *models.Ke
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateKeyTokenLastUsed(ctx TxContext, keyTokenid models.KeyTokenID) error {
|
||||
func (db *Database) UpdateKeyTokenLastUsed(ctx db.TxContext, keyTokenid models.KeyTokenID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package primary
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
|
@ -9,7 +10,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*models.Message, error) {
|
||||
func (db *Database) GetMessageByUserMessageID(ctx db.TxContext, usrMsgId string) (*models.Message, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -21,7 +22,7 @@ func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*
|
|||
}
|
||||
|
||||
msg, err := models.DecodeMessage(rows)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -31,7 +32,7 @@ func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*
|
|||
return &msg, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetMessage(ctx TxContext, scnMessageID models.MessageID, allowDeleted bool) (models.Message, error) {
|
||||
func (db *Database) GetMessage(ctx db.TxContext, scnMessageID models.MessageID, allowDeleted bool) (models.Message, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.Message{}, err
|
||||
|
@ -57,7 +58,7 @@ func (db *Database) GetMessage(ctx TxContext, scnMessageID models.MessageID, all
|
|||
return msg, nil
|
||||
}
|
||||
|
||||
func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, channel models.Channel, timestampSend *time.Time, title string, content *string, priority int, userMsgId *string, senderIP string, senderName *string, usedKeyID models.KeyTokenID) (models.Message, error) {
|
||||
func (db *Database) CreateMessage(ctx db.TxContext, senderUserID models.UserID, channel models.Channel, timestampSend *time.Time, title string, content *string, priority int, userMsgId *string, senderIP string, senderName *string, usedKeyID models.KeyTokenID) (models.Message, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.Message{}, err
|
||||
|
@ -66,7 +67,6 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
|
|||
entity := models.MessageDB{
|
||||
MessageID: models.NewMessageID(),
|
||||
SenderUserID: senderUserID,
|
||||
OwnerUserID: channel.OwnerUserID,
|
||||
ChannelInternalName: channel.InternalName,
|
||||
ChannelID: channel.ChannelID,
|
||||
SenderIP: senderIP,
|
||||
|
@ -89,7 +89,7 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
|
|||
return entity.Model(), nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteMessage(ctx TxContext, messageID models.MessageID) error {
|
||||
func (db *Database) DeleteMessage(ctx db.TxContext, messageID models.MessageID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -103,7 +103,7 @@ func (db *Database) DeleteMessage(ctx TxContext, messageID models.MessageID) err
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) ListMessages(ctx TxContext, filter models.MessageFilter, pageSize *int, inTok ct.CursorToken) ([]models.Message, ct.CursorToken, error) {
|
||||
func (db *Database) ListMessages(ctx db.TxContext, filter models.MessageFilter, pageSize *int, inTok ct.CursorToken) ([]models.Message, ct.CursorToken, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, ct.CursorToken{}, err
|
||||
|
@ -151,7 +151,7 @@ func (db *Database) ListMessages(ctx TxContext, filter models.MessageFilter, pag
|
|||
}
|
||||
}
|
||||
|
||||
func (db *Database) CountMessages(ctx TxContext, filter models.MessageFilter) (int64, error) {
|
||||
func (db *Database) CountMessages(ctx db.TxContext, filter models.MessageFilter) (int64, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
package primary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"errors"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
)
|
||||
|
||||
func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
|
||||
func (db *Database) ReadSchema(ctx db.TxContext) (retval int, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
r1, err := db.db.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
|
||||
r1, err := tx.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -31,7 +35,7 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
|
|||
return 0, err
|
||||
}
|
||||
|
||||
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
|
||||
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -62,8 +66,13 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
|
|||
return dbschema, nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
func (db *Database) WriteMetaString(ctx db.TxContext, key string, value string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -73,8 +82,13 @@ func (db *Database) WriteMetaString(ctx context.Context, key string, value strin
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
func (db *Database) WriteMetaInt(ctx db.TxContext, key string, value int64) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -84,8 +98,13 @@ func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
|
||||
func (db *Database) WriteMetaReal(ctx db.TxContext, key string, value float64) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -95,8 +114,13 @@ func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
|
||||
func (db *Database) WriteMetaBlob(ctx db.TxContext, key string, value []byte) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -106,8 +130,13 @@ func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *string, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaString(ctx db.TxContext, key string) (retval *string, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -137,8 +166,13 @@ func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *str
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaInt(ctx db.TxContext, key string) (retval *int64, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -169,8 +203,13 @@ func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64,
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float64, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaReal(ctx db.TxContext, key string) (retval *float64, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -201,8 +240,13 @@ func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byte, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaBlob(ctx db.TxContext, key string) (retval *[]byte, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -233,8 +277,13 @@ func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byt
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteMeta(ctx context.Context, key string) error {
|
||||
_, err := db.db.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) DeleteMeta(ctx db.TxContext, key string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package primary
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (db *Database) CreateSubscription(ctx TxContext, subscriberUID models.UserID, channel models.Channel, confirmed bool) (models.Subscription, error) {
|
||||
func (db *Database) CreateSubscription(ctx db.TxContext, subscriberUID models.UserID, channel models.Channel, confirmed bool) (models.Subscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.Subscription{}, err
|
||||
|
@ -31,15 +33,19 @@ func (db *Database) CreateSubscription(ctx TxContext, subscriberUID models.UserI
|
|||
return entity.Model(), nil
|
||||
}
|
||||
|
||||
func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID models.ChannelID) ([]models.Subscription, error) {
|
||||
func (db *Database) ListSubscriptions(ctx db.TxContext, filter models.SubscriptionFilter) ([]models.Subscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
order := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC "
|
||||
filterCond, filterJoin, prepParams, err := filter.SQL()
|
||||
|
||||
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE channel_id = :cid"+order, sq.PP{"cid": channelID})
|
||||
orderClause := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC "
|
||||
|
||||
sqlQuery := "SELECT " + "subscriptions.*" + " FROM subscriptions " + filterJoin + " WHERE ( " + filterCond + " ) " + orderClause
|
||||
|
||||
rows, err := tx.Query(ctx, sqlQuery, prepParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -52,63 +58,7 @@ func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID models.C
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (db *Database) ListSubscriptionsByChannelOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cond := ""
|
||||
if confirmed != nil && *confirmed {
|
||||
cond = " AND confirmed = 1"
|
||||
} else if confirmed != nil && !*confirmed {
|
||||
cond = " AND confirmed = 0"
|
||||
}
|
||||
|
||||
order := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC "
|
||||
|
||||
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE channel_owner_user_id = :ouid"+cond+order, sq.PP{"ouid": ownerUserID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := models.DecodeSubscriptions(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *Database) ListSubscriptionsBySubscriber(ctx TxContext, subscriberUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cond := ""
|
||||
if confirmed != nil && *confirmed {
|
||||
cond = " AND confirmed = 1"
|
||||
} else if confirmed != nil && !*confirmed {
|
||||
cond = " AND confirmed = 0"
|
||||
}
|
||||
|
||||
order := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC "
|
||||
|
||||
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE subscriber_user_id = :suid"+cond+order, sq.PP{"suid": subscriberUserID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := models.DecodeSubscriptions(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetSubscription(ctx TxContext, subid models.SubscriptionID) (models.Subscription, error) {
|
||||
func (db *Database) GetSubscription(ctx db.TxContext, subid models.SubscriptionID) (models.Subscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.Subscription{}, err
|
||||
|
@ -127,7 +77,7 @@ func (db *Database) GetSubscription(ctx TxContext, subid models.SubscriptionID)
|
|||
return sub, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetSubscriptionBySubscriber(ctx TxContext, subscriberId models.UserID, channelId models.ChannelID) (*models.Subscription, error) {
|
||||
func (db *Database) GetSubscriptionBySubscriber(ctx db.TxContext, subscriberId models.UserID, channelId models.ChannelID) (*models.Subscription, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -142,7 +92,7 @@ func (db *Database) GetSubscriptionBySubscriber(ctx TxContext, subscriberId mode
|
|||
}
|
||||
|
||||
user, err := models.DecodeSubscription(rows)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -152,7 +102,7 @@ func (db *Database) GetSubscriptionBySubscriber(ctx TxContext, subscriberId mode
|
|||
return &user, nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteSubscription(ctx TxContext, subid models.SubscriptionID) error {
|
||||
func (db *Database) DeleteSubscription(ctx db.TxContext, subid models.SubscriptionID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -166,7 +116,7 @@ func (db *Database) DeleteSubscription(ctx TxContext, subid models.SubscriptionI
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateSubscriptionConfirmed(ctx TxContext, subscriptionID models.SubscriptionID, confirmed bool) error {
|
||||
func (db *Database) UpdateSubscriptionConfirmed(ctx db.TxContext, subscriptionID models.SubscriptionID, confirmed bool) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -2,13 +2,14 @@ package primary
|
|||
|
||||
import (
|
||||
scn "blackforestbytes.com/simplecloudnotifier"
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (db *Database) CreateUser(ctx TxContext, protoken *string, username *string) (models.User, error) {
|
||||
func (db *Database) CreateUser(ctx db.TxContext, protoken *string, username *string) (models.User, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
|
@ -35,7 +36,7 @@ func (db *Database) CreateUser(ctx TxContext, protoken *string, username *string
|
|||
return entity.Model(), nil
|
||||
}
|
||||
|
||||
func (db *Database) ClearProTokens(ctx TxContext, protoken string) error {
|
||||
func (db *Database) ClearProTokens(ctx db.TxContext, protoken string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -49,7 +50,7 @@ func (db *Database) ClearProTokens(ctx TxContext, protoken string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) GetUser(ctx TxContext, userid models.UserID) (models.User, error) {
|
||||
func (db *Database) GetUser(ctx db.TxContext, userid models.UserID) (models.User, error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
|
@ -68,7 +69,7 @@ func (db *Database) GetUser(ctx TxContext, userid models.UserID) (models.User, e
|
|||
return user, nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateUserUsername(ctx TxContext, userid models.UserID, username *string) error {
|
||||
func (db *Database) UpdateUserUsername(ctx db.TxContext, userid models.UserID, username *string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -85,7 +86,7 @@ func (db *Database) UpdateUserUsername(ctx TxContext, userid models.UserID, user
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateUserProToken(ctx TxContext, userid models.UserID, protoken *string) error {
|
||||
func (db *Database) UpdateUserProToken(ctx db.TxContext, userid models.UserID, protoken *string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -103,7 +104,7 @@ func (db *Database) UpdateUserProToken(ctx TxContext, userid models.UserID, prot
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) IncUserMessageCounter(ctx TxContext, user *models.User) error {
|
||||
func (db *Database) IncUserMessageCounter(ctx db.TxContext, user *models.User) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -132,7 +133,7 @@ func (db *Database) IncUserMessageCounter(ctx TxContext, user *models.User) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateUserLastRead(ctx TxContext, userid models.UserID) error {
|
||||
func (db *Database) UpdateUserLastRead(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
server "blackforestbytes.com/simplecloudnotifier"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/dbtools"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/schema"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
@ -63,77 +64,98 @@ func (db *Database) DB() sq.DB {
|
|||
return db.db
|
||||
}
|
||||
|
||||
func (db *Database) Migrate(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second)
|
||||
defer cancel()
|
||||
func (db *Database) Migrate(outerctx context.Context) error {
|
||||
innerctx, cancel := context.WithTimeout(outerctx, 24*time.Second)
|
||||
tctx := simplectx.CreateSimpleContext(innerctx, cancel)
|
||||
|
||||
currschema, err := db.ReadSchema(ctx)
|
||||
tx, err := tctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if tx.Status() == sq.TxStatusInitial || tx.Status() == sq.TxStatusActive {
|
||||
err = tx.Rollback()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to rollback transaction")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ppReInit := false
|
||||
|
||||
currschema, err := db.ReadSchema(tctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if currschema == 0 {
|
||||
schemastr := schema.RequestsSchema[schema.RequestsSchemaVersion].SQL
|
||||
schemahash := schema.RequestsSchema[schema.RequestsSchemaVersion].Hash
|
||||
|
||||
schemastr := schema.RequestsSchema1
|
||||
|
||||
schemahash, err := sq.HashSqliteSchema(ctx, schemastr)
|
||||
schemahash, err := sq.HashSqliteSchema(tctx, schemastr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.db.Exec(ctx, schemastr, sq.PP{})
|
||||
_, err = tx.Exec(tctx, schemastr, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WriteMetaInt(ctx, "schema", 1)
|
||||
err = db.WriteMetaInt(tctx, "schema", int64(schema.RequestsSchemaVersion))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WriteMetaString(ctx, "schema_hash", schemahash)
|
||||
err = db.WriteMetaString(tctx, "schema_hash", schemahash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.pp.Init(ctx) // Re-Init
|
||||
ppReInit = true
|
||||
|
||||
currschema = schema.LogsSchemaVersion
|
||||
}
|
||||
|
||||
if currschema == 1 {
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
} else if currschema == 1 {
|
||||
|
||||
schemHashDB, err := sq.HashSqliteDatabase(ctx, db.db)
|
||||
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemaHashMeta, err := db.ReadMetaString(ctx, "schema_hash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemHashAsset := schema.RequestsHash1
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schemHashAsset {
|
||||
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.RequestsSchema[currschema].Hash {
|
||||
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (requests db)")
|
||||
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).Msg("Schema (requests db)")
|
||||
log.Debug().Str("schemHashAsset", schemHashAsset).Msg("Schema (requests db)")
|
||||
log.Debug().Str("schemaHashAsset", schema.RequestsSchema[currschema].Hash).Msg("Schema (requests db)")
|
||||
return errors.New("database schema does not match (requests db)")
|
||||
} else {
|
||||
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (requests db)")
|
||||
}
|
||||
}
|
||||
|
||||
return nil // current
|
||||
} else {
|
||||
if currschema != schema.RequestsSchemaVersion {
|
||||
return errors.New(fmt.Sprintf("Unknown DB schema: %d", currschema))
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ppReInit {
|
||||
log.Debug().Msg("Re-Init preprocessor")
|
||||
err = db.pp.Init(outerctx) // Re-Init
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) Ping(ctx context.Context) error {
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
package requests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"errors"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
)
|
||||
|
||||
func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
|
||||
func (db *Database) ReadSchema(ctx db.TxContext) (retval int, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
r1, err := db.db.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
|
||||
r1, err := tx.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -31,7 +35,7 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
|
|||
return 0, err
|
||||
}
|
||||
|
||||
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
|
||||
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -62,8 +66,13 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
|
|||
return dbschema, nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
func (db *Database) WriteMetaString(ctx db.TxContext, key string, value string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -73,8 +82,13 @@ func (db *Database) WriteMetaString(ctx context.Context, key string, value strin
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
func (db *Database) WriteMetaInt(ctx db.TxContext, key string, value int64) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -84,8 +98,13 @@ func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
|
||||
func (db *Database) WriteMetaReal(ctx db.TxContext, key string, value float64) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -95,8 +114,13 @@ func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error {
|
||||
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
|
||||
func (db *Database) WriteMetaBlob(ctx db.TxContext, key string, value []byte) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
|
||||
"key": key,
|
||||
"val": value,
|
||||
})
|
||||
|
@ -106,8 +130,13 @@ func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *string, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaString(ctx db.TxContext, key string) (retval *string, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -137,8 +166,13 @@ func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *str
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaInt(ctx db.TxContext, key string) (retval *int64, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -169,8 +203,13 @@ func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64,
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float64, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaReal(ctx db.TxContext, key string) (retval *float64, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -201,8 +240,13 @@ func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byte, reterr error) {
|
||||
r2, err := db.db.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) ReadMetaBlob(ctx db.TxContext, key string) (retval *[]byte, reterr error) {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2, err := tx.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -233,8 +277,13 @@ func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byt
|
|||
return langext.Ptr(value), nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteMeta(ctx context.Context, key string) error {
|
||||
_, err := db.db.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
func (db *Database) DeleteMeta(ctx db.TxContext, key string) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -2,27 +2,52 @@ package schema
|
|||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed primary_1.ddl
|
||||
var PrimarySchema1 string
|
||||
type Def struct {
|
||||
SQL string
|
||||
Hash string
|
||||
}
|
||||
|
||||
const PrimaryHash1 = "f2b2847f32681a7178e405553beea4a324034915a0c5a5dc70b3c6abbcc852f2"
|
||||
//go:embed primary_1.ddl
|
||||
var primarySchema1 string
|
||||
|
||||
//go:embed primary_2.ddl
|
||||
var PrimarySchema2 string
|
||||
|
||||
const PrimaryHash2 = "07ed1449114416ed043084a30e0722a5f97bf172161338d2f7106a8dfd387d0a"
|
||||
var primarySchema2 string
|
||||
|
||||
//go:embed primary_3.ddl
|
||||
var PrimarySchema3 string
|
||||
var primarySchema3 string
|
||||
|
||||
const PrimaryHash3 = "65c2125ad0e12d02490cf2275f0067ef3c62a8522edf9a35ee8aa3f3c09b12e8"
|
||||
//go:embed primary_4.ddl
|
||||
var primarySchema4 string
|
||||
|
||||
//go:embed primary_migration_3_4.ddl
|
||||
var PrimaryMigration_3_4 string
|
||||
|
||||
//go:embed requests_1.ddl
|
||||
var RequestsSchema1 string
|
||||
|
||||
const RequestsHash1 = "ebb0a5748b605e8215437413b738279670190ca8159b6227cfc2aa13418b41e9"
|
||||
var requestsSchema1 string
|
||||
|
||||
//go:embed logs_1.ddl
|
||||
var LogsSchema1 string
|
||||
var logsSchema1 string
|
||||
|
||||
const LogsHash1 = "65fba477c04095effc3a8e1bb79fe7547b8e52e983f776f156266eddc4f201d7"
|
||||
var PrimarySchema = map[int]Def{
|
||||
0: {"", ""},
|
||||
1: {primarySchema1, "f2b2847f32681a7178e405553beea4a324034915a0c5a5dc70b3c6abbcc852f2"},
|
||||
2: {primarySchema2, "07ed1449114416ed043084a30e0722a5f97bf172161338d2f7106a8dfd387d0a"},
|
||||
3: {primarySchema3, "65c2125ad0e12d02490cf2275f0067ef3c62a8522edf9a35ee8aa3f3c09b12e8"},
|
||||
4: {primarySchema4, "cb022156ab0e7aea39dd0c985428c43cae7d60e41ca8e9e5a84c774b3019d2ca"},
|
||||
}
|
||||
|
||||
var PrimarySchemaVersion = 4
|
||||
|
||||
var RequestsSchema = map[int]Def{
|
||||
0: {"", ""},
|
||||
1: {requestsSchema1, "ebb0a5748b605e8215437413b738279670190ca8159b6227cfc2aa13418b41e9"},
|
||||
}
|
||||
|
||||
var RequestsSchemaVersion = 1
|
||||
|
||||
var LogsSchema = map[int]Def{
|
||||
0: {"", ""},
|
||||
1: {logsSchema1, "65fba477c04095effc3a8e1bb79fe7547b8e52e983f776f156266eddc4f201d7"},
|
||||
}
|
||||
|
||||
var LogsSchemaVersion = 1
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
CREATE TABLE users
|
||||
(
|
||||
user_id TEXT NOT NULL,
|
||||
|
||||
username TEXT NULL DEFAULT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_lastread INTEGER NULL DEFAULT NULL,
|
||||
timestamp_lastsent INTEGER NULL DEFAULT NULL,
|
||||
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
|
||||
quota_used INTEGER NOT NULL DEFAULT '0',
|
||||
quota_used_day TEXT NULL DEFAULT NULL,
|
||||
|
||||
is_pro INTEGER CHECK(is_pro IN (0, 1)) NOT NULL DEFAULT 0,
|
||||
pro_token TEXT NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (user_id)
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_users_protoken" ON users (pro_token) WHERE pro_token IS NOT NULL;
|
||||
|
||||
|
||||
CREATE TABLE keytokens
|
||||
(
|
||||
keytoken_id TEXT NOT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_lastused INTEGER NULL DEFAULT NULL,
|
||||
|
||||
name TEXT NOT NULL,
|
||||
|
||||
owner_user_id TEXT NOT NULL,
|
||||
|
||||
all_channels INTEGER CHECK(all_channels IN (0, 1)) NOT NULL,
|
||||
channels TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
permissions TEXT NOT NULL,
|
||||
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (keytoken_id)
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_keytokens_token" ON keytokens (token);
|
||||
|
||||
|
||||
CREATE TABLE clients
|
||||
(
|
||||
client_id TEXT NOT NULL,
|
||||
|
||||
user_id TEXT NOT NULL,
|
||||
type TEXT CHECK(type IN ('ANDROID', 'IOS')) NOT NULL,
|
||||
fcm_token TEXT NOT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
|
||||
agent_model TEXT NOT NULL,
|
||||
agent_version TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (client_id)
|
||||
) STRICT;
|
||||
CREATE INDEX "idx_clients_userid" ON clients (user_id);
|
||||
CREATE UNIQUE INDEX "idx_clients_fcmtoken" ON clients (fcm_token);
|
||||
|
||||
|
||||
CREATE TABLE channels
|
||||
(
|
||||
channel_id TEXT NOT NULL,
|
||||
|
||||
owner_user_id TEXT NOT NULL,
|
||||
|
||||
internal_name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description_name TEXT NULL,
|
||||
|
||||
subscribe_key TEXT NOT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_lastsent INTEGER NULL DEFAULT NULL,
|
||||
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (channel_id)
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, internal_name);
|
||||
|
||||
CREATE TABLE subscriptions
|
||||
(
|
||||
subscription_id TEXT NOT NULL,
|
||||
|
||||
subscriber_user_id TEXT NOT NULL,
|
||||
channel_owner_user_id TEXT NOT NULL,
|
||||
channel_internal_name TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
|
||||
confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL,
|
||||
|
||||
PRIMARY KEY (subscription_id)
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_internal_name);
|
||||
CREATE INDEX "idx_subscriptions_chan" ON subscriptions (channel_id);
|
||||
CREATE INDEX "idx_subscriptions_subuser" ON subscriptions (subscriber_user_id);
|
||||
CREATE INDEX "idx_subscriptions_ownuser" ON subscriptions (channel_owner_user_id);
|
||||
CREATE INDEX "idx_subscriptions_tsc" ON subscriptions (timestamp_created);
|
||||
CREATE INDEX "idx_subscriptions_conf" ON subscriptions (confirmed);
|
||||
|
||||
|
||||
CREATE TABLE messages
|
||||
(
|
||||
message_id TEXT NOT NULL,
|
||||
sender_user_id TEXT NOT NULL,
|
||||
channel_internal_name TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
sender_ip TEXT NOT NULL,
|
||||
sender_name TEXT NULL,
|
||||
|
||||
timestamp_real INTEGER NOT NULL,
|
||||
timestamp_client INTEGER NULL,
|
||||
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NULL,
|
||||
priority INTEGER CHECK(priority IN (0, 1, 2)) NOT NULL,
|
||||
usr_message_id TEXT NULL,
|
||||
|
||||
used_key_id TEXT NOT NULL,
|
||||
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (message_id)
|
||||
) STRICT;
|
||||
CREATE INDEX "idx_messages_channel" ON messages (channel_internal_name COLLATE BINARY);
|
||||
CREATE INDEX "idx_messages_channel_nc" ON messages (channel_internal_name COLLATE NOCASE);
|
||||
CREATE UNIQUE INDEX "idx_messages_idempotency" ON messages (sender_user_id, usr_message_id COLLATE BINARY);
|
||||
CREATE INDEX "idx_messages_senderip" ON messages (sender_ip COLLATE BINARY);
|
||||
CREATE INDEX "idx_messages_sendername" ON messages (sender_name COLLATE BINARY);
|
||||
CREATE INDEX "idx_messages_sendername_nc" ON messages (sender_name COLLATE NOCASE);
|
||||
CREATE INDEX "idx_messages_title" ON messages (title COLLATE BINARY);
|
||||
CREATE INDEX "idx_messages_title_nc" ON messages (title COLLATE NOCASE);
|
||||
CREATE INDEX "idx_messages_usedkey" ON messages (sender_user_id, used_key_id);
|
||||
CREATE INDEX "idx_messages_deleted" ON messages (deleted);
|
||||
|
||||
|
||||
CREATE VIRTUAL TABLE messages_fts USING fts5
|
||||
(
|
||||
channel_internal_name,
|
||||
sender_name,
|
||||
title,
|
||||
content,
|
||||
|
||||
tokenize = unicode61,
|
||||
content = 'messages',
|
||||
content_rowid = 'rowid'
|
||||
);
|
||||
|
||||
CREATE TRIGGER fts_insert AFTER INSERT ON messages BEGIN
|
||||
INSERT INTO messages_fts (rowid, channel_internal_name, sender_name, title, content) VALUES (new.rowid, new.channel_internal_name, new.sender_name, new.title, new.content);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER fts_update AFTER UPDATE ON messages BEGIN
|
||||
INSERT INTO messages_fts (messages_fts, rowid, channel_internal_name, sender_name, title, content) VALUES ('delete', old.rowid, old.channel_internal_name, old.sender_name, old.title, old.content);
|
||||
INSERT INTO messages_fts ( rowid, channel_internal_name, sender_name, title, content) VALUES ( new.rowid, new.channel_internal_name, new.sender_name, new.title, new.content);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER fts_delete AFTER DELETE ON messages BEGIN
|
||||
INSERT INTO messages_fts (messages_fts, rowid, channel_internal_name, sender_name, title, content) VALUES ('delete', old.rowid, old.channel_internal_name, old.sender_name, old.title, old.content);
|
||||
END;
|
||||
|
||||
|
||||
CREATE TABLE deliveries
|
||||
(
|
||||
delivery_id TEXT NOT NULL,
|
||||
|
||||
message_id TEXT NOT NULL,
|
||||
receiver_user_id TEXT NOT NULL,
|
||||
receiver_client_id TEXT NOT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_finalized INTEGER NULL,
|
||||
|
||||
|
||||
status TEXT CHECK(status IN ('RETRY','SUCCESS','FAILED')) NOT NULL,
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
next_delivery TEXT NULL DEFAULT NULL,
|
||||
|
||||
fcm_message_id TEXT NULL,
|
||||
|
||||
PRIMARY KEY (delivery_id)
|
||||
) STRICT;
|
||||
CREATE INDEX "idx_deliveries_receiver" ON deliveries (message_id, receiver_client_id);
|
||||
|
||||
|
||||
CREATE TABLE compat_ids
|
||||
(
|
||||
old INTEGER NOT NULL,
|
||||
new TEXT NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_compatids_new" ON compat_ids (new);
|
||||
CREATE UNIQUE INDEX "idx_compatids_old" ON compat_ids (old, type);
|
||||
|
||||
|
||||
CREATE TABLE compat_acks
|
||||
(
|
||||
user_id TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE INDEX "idx_compatacks_userid" ON compat_acks (user_id);
|
||||
CREATE UNIQUE INDEX "idx_compatacks_messageid" ON compat_acks (message_id);
|
||||
CREATE UNIQUE INDEX "idx_compatacks_userid_messageid" ON compat_acks (user_id, message_id);
|
||||
|
||||
|
||||
CREATE TABLE compat_clients
|
||||
(
|
||||
client_id TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_compatclient_clientid" ON compat_clients (client_id);
|
||||
|
||||
|
||||
CREATE TABLE `meta`
|
||||
(
|
||||
meta_key TEXT NOT NULL,
|
||||
value_int INTEGER NULL,
|
||||
value_txt TEXT NULL,
|
||||
value_real REAL NULL,
|
||||
value_blob BLOB NULL,
|
||||
|
||||
PRIMARY KEY (meta_key)
|
||||
) STRICT;
|
||||
|
||||
|
||||
INSERT INTO meta (meta_key, value_int) VALUES ('schema', 3)
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
DROP INDEX idx_messages_owner_channel;
|
||||
|
||||
|
||||
DROP INDEX idx_messages_owner_channel_nc;
|
||||
|
||||
|
||||
DROP INDEX idx_messages_idempotency;
|
||||
CREATE UNIQUE INDEX "idx_messages_idempotency" ON messages (sender_user_id, usr_message_id COLLATE BINARY);
|
||||
|
||||
|
||||
DROP INDEX idx_messages_usedkey;
|
||||
CREATE INDEX "idx_messages_usedkey" ON messages (sender_user_id, used_key_id);
|
||||
|
||||
|
||||
ALTER TABLE messages DROP COLUMN owner_user_id;
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package logic
|
||||
package simplectx
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
|
@ -51,7 +51,9 @@ func (sc *SimpleContext) Cancel() {
|
|||
}
|
||||
sc.transaction = nil
|
||||
}
|
||||
sc.cancelFunc()
|
||||
if sc.cancelFunc != nil {
|
||||
sc.cancelFunc()
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *SimpleContext) GetOrCreateTransaction(db db.DatabaseImpl) (sq.Tx, error) {
|
|
@ -9,13 +9,14 @@ require (
|
|||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/rs/zerolog v1.29.1
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.163
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.218
|
||||
gopkg.in/loremipsum.v1 v1.1.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/bytedance/sonic v1.10.0-rc3 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
|
@ -29,15 +30,15 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.10.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/term v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/term v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0=
|
||||
github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
|
@ -39,6 +46,7 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
|||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
|
@ -60,6 +68,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
@ -77,19 +87,32 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.163 h1:GYC34wLOdBM/CgAov0AyznfHGd09Km106Ijmp8cZmp4=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.163/go.mod h1:Tood+vqmPqS/meYRnUcGz837wqHkP8BykVpY1h8TWoI=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.216 h1:cr28oRYNBMNohRyjb0nHNZg0OLE/NVq82zbkVYuNS1M=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.216/go.mod h1:eBL+dE41RaoQw38oLVzxSxLPsFq6sGFug7o3i7Br+SY=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.217 h1:Z0vLwOgCzVi3nJLriPRBtFTyXhC9KjeAmk2DqvT7gPM=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.217/go.mod h1:eBL+dE41RaoQw38oLVzxSxLPsFq6sGFug7o3i7Br+SY=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.218 h1:ly69FPNGqcTc1o6Qf+P6HpHtKmOxG1/fWLj/Aqzi5Rg=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.218/go.mod h1:eBL+dE41RaoQw38oLVzxSxLPsFq6sGFug7o3i7Br+SY=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -97,14 +120,22 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/loremipsum.v1 v1.1.2 h1:12APklfJKuGszqZsrArW5QoQh03/W+qyCCjvnDuS6Tw=
|
||||
|
@ -112,4 +143,5 @@ gopkg.in/loremipsum.v1 v1.1.2/go.mod h1:TuRvzFuzuejXj+odBU6Tubp/EPUyGb9wmSvHenyP
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package jobs
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
|
||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"errors"
|
||||
|
@ -132,7 +133,7 @@ func (j *DeliveryRetryJob) execute() (fastrr bool, err error) {
|
|||
return len(deliveries) == 32, nil
|
||||
}
|
||||
|
||||
func (j *DeliveryRetryJob) redeliver(ctx *logic.SimpleContext, delivery models.Delivery) {
|
||||
func (j *DeliveryRetryJob) redeliver(ctx *simplectx.SimpleContext, delivery models.Delivery) {
|
||||
|
||||
client, err := j.app.Database.Primary.GetClient(ctx, delivery.ReceiverUserID, delivery.ReceiverClientID)
|
||||
if err != nil {
|
||||
|
@ -168,7 +169,7 @@ func (j *DeliveryRetryJob) redeliver(ctx *logic.SimpleContext, delivery models.D
|
|||
var msgidOverride *string = nil
|
||||
if isCompatClient {
|
||||
|
||||
messageIdComp, err := j.app.Database.Primary.ConvertToCompatIDOrCreate(ctx, msg.MessageID.String(), "messageid")
|
||||
messageIdComp, err := j.app.Database.Primary.ConvertToCompatIDOrCreate(ctx, "messageid", msg.MessageID.String())
|
||||
if err != nil {
|
||||
log.Err(err).Str("MessageID", delivery.MessageID.String()).Str("ClientID", client.ClientID.String()).Msg("Failed to query/create messageid")
|
||||
ctx.RollbackTransaction()
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
scn "blackforestbytes.com/simplecloudnotifier"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/impl/primary"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
|
||||
"blackforestbytes.com/simplecloudnotifier/google"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"blackforestbytes.com/simplecloudnotifier/push"
|
||||
|
@ -164,14 +166,6 @@ func (app *Application) GenerateRandomAuthKey() string {
|
|||
return scn.RandomAuthKey()
|
||||
}
|
||||
|
||||
func (app *Application) QuotaMax(ispro bool) int {
|
||||
if ispro {
|
||||
return 1000
|
||||
} else {
|
||||
return 50
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) VerifyProToken(ctx *AppContext, token string) (bool, error) {
|
||||
|
||||
if strings.HasPrefix(token, "ANDROID|v1|") {
|
||||
|
@ -292,9 +286,9 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an
|
|||
return actx, nil
|
||||
}
|
||||
|
||||
func (app *Application) NewSimpleTransactionContext(timeout time.Duration) *SimpleContext {
|
||||
func (app *Application) NewSimpleTransactionContext(timeout time.Duration) *simplectx.SimpleContext {
|
||||
ictx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
return CreateSimpleContext(ictx, cancel)
|
||||
return simplectx.CreateSimpleContext(ictx, cancel)
|
||||
}
|
||||
|
||||
func (app *Application) getPermissions(ctx *AppContext, hdr string) (models.PermissionSet, error) {
|
||||
|
@ -338,7 +332,13 @@ func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID
|
|||
|
||||
subscribeKey := app.GenerateRandomAuthKey()
|
||||
|
||||
newChan, err := app.Database.Primary.CreateChannel(ctx, userid, displayChanName, intChanName, subscribeKey)
|
||||
channel := primary.CreateChanel{
|
||||
UserId: userid,
|
||||
IntName: intChanName,
|
||||
SubscribeKey: subscribeKey,
|
||||
DisplayName: displayChanName,
|
||||
}
|
||||
newChan, err := app.Database.Primary.CreateChannel(ctx, channel)
|
||||
if err != nil {
|
||||
return models.Channel{}, err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
package logic
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SendMessageResponse struct {
|
||||
User models.User
|
||||
Message models.Message
|
||||
MessageIsOld bool
|
||||
CompatMessageID int64
|
||||
}
|
||||
|
||||
func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *models.UserID, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginresp.HTTPResponse) {
|
||||
if Title != nil {
|
||||
Title = langext.Ptr(strings.TrimSpace(*Title))
|
||||
}
|
||||
if UserMessageID != nil {
|
||||
UserMessageID = langext.Ptr(strings.TrimSpace(*UserMessageID))
|
||||
}
|
||||
|
||||
if UserID == nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_UID, hl.USER_ID, "Missing parameter [[user_id]]", nil))
|
||||
}
|
||||
if Key == nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[key]]", nil))
|
||||
}
|
||||
if Title == nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TITLE, hl.TITLE, "Missing parameter [[title]]", nil))
|
||||
}
|
||||
if Priority != nil && (*Priority != 0 && *Priority != 1 && *Priority != 2) {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.INVALID_PRIO, hl.PRIORITY, "Invalid priority", nil))
|
||||
}
|
||||
if len(*Title) == 0 {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.NO_TITLE, hl.TITLE, "No title specified", nil))
|
||||
}
|
||||
|
||||
user, err := app.Database.Primary.GetUser(ctx, *UserID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", err))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err))
|
||||
}
|
||||
|
||||
channelDisplayName := user.DefaultChannel()
|
||||
channelInternalName := user.DefaultChannel()
|
||||
if Channel != nil {
|
||||
channelDisplayName = app.NormalizeChannelDisplayName(*Channel)
|
||||
channelInternalName = app.NormalizeChannelInternalName(*Channel)
|
||||
}
|
||||
|
||||
if len(*Title) > user.MaxTitleLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.TITLE_TOO_LONG, hl.TITLE, fmt.Sprintf("Title too long (max %d characters)", user.MaxTitleLength()), nil))
|
||||
}
|
||||
if Content != nil && len(*Content) > user.MaxContentLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, hl.CONTENT, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil))
|
||||
}
|
||||
if len(channelDisplayName) > user.MaxChannelNameLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil))
|
||||
}
|
||||
if len(strings.TrimSpace(channelDisplayName)) == 0 {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_NAME_EMPTY, hl.CHANNEL, fmt.Sprintf("Channel displayname cannot be empty"), nil))
|
||||
}
|
||||
if len(channelInternalName) > user.MaxChannelNameLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil))
|
||||
}
|
||||
if len(strings.TrimSpace(channelInternalName)) == 0 {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_NAME_EMPTY, hl.CHANNEL, fmt.Sprintf("Channel internalname cannot be empty"), nil))
|
||||
}
|
||||
if SenderName != nil && len(*SenderName) > user.MaxSenderNameLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.SENDERNAME_TOO_LONG, hl.SENDER_NAME, fmt.Sprintf("SenderName too long (max %d characters)", user.MaxSenderNameLength()), nil))
|
||||
}
|
||||
if UserMessageID != nil && len(*UserMessageID) > user.MaxUserMessageIDLength() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.USR_MSG_ID_TOO_LONG, hl.USER_MESSAGE_ID, fmt.Sprintf("MessageID too long (max %d characters)", user.MaxUserMessageIDLength()), nil))
|
||||
}
|
||||
if SendTimestamp != nil && mathext.Abs(*SendTimestamp-float64(time.Now().Unix())) > timeext.FromHours(user.MaxTimestampDiffHours()).Seconds() {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.TIMESTAMP_OUT_OF_RANGE, hl.NONE, fmt.Sprintf("The timestamp mus be within %d hours of now()", user.MaxTimestampDiffHours()), nil))
|
||||
}
|
||||
|
||||
if UserMessageID != nil {
|
||||
msg, err := app.Database.Primary.GetMessageByUserMessageID(ctx, *UserMessageID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query existing message", err))
|
||||
}
|
||||
if msg != nil {
|
||||
|
||||
existingCompID, _, err := app.Database.Primary.ConvertToCompatID(ctx, msg.MessageID.String())
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query compat-id", err))
|
||||
}
|
||||
|
||||
if existingCompID == nil {
|
||||
v, err := app.Database.Primary.CreateCompatID(ctx, "messageid", msg.MessageID.String())
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
|
||||
}
|
||||
existingCompID = &v
|
||||
}
|
||||
|
||||
//the found message can be deleted (!), but we still return NO_ERROR here...
|
||||
return &SendMessageResponse{
|
||||
User: user,
|
||||
Message: *msg,
|
||||
MessageIsOld: true,
|
||||
CompatMessageID: *existingCompID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
if user.QuotaRemainingToday() <= 0 {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, hl.NONE, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil))
|
||||
}
|
||||
|
||||
channel, err := app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err))
|
||||
}
|
||||
|
||||
keytok, permResp := ctx.CheckPermissionSend(channel, *Key)
|
||||
if permResp != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, hl.USER_KEY, "You are not authorized for this action", nil))
|
||||
}
|
||||
|
||||
var sendTimestamp *time.Time = nil
|
||||
if SendTimestamp != nil {
|
||||
sendTimestamp = langext.Ptr(timeext.UnixFloatSeconds(*SendTimestamp))
|
||||
}
|
||||
|
||||
priority := langext.Coalesce(Priority, user.DefaultPriority())
|
||||
|
||||
clientIP := g.ClientIP()
|
||||
|
||||
msg, err := app.Database.Primary.CreateMessage(ctx, *UserID, channel, sendTimestamp, *Title, Content, priority, UserMessageID, clientIP, SenderName, keytok.KeyTokenID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create message in db", err))
|
||||
}
|
||||
|
||||
compatMsgID, err := app.Database.Primary.CreateCompatID(ctx, "messageid", msg.MessageID.String())
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
|
||||
}
|
||||
|
||||
subFilter := models.SubscriptionFilter{ChannelID: langext.Ptr([]models.ChannelID{channel.ChannelID}), Confirmed: langext.PTrue}
|
||||
activeSubscriptions, err := app.Database.Primary.ListSubscriptions(ctx, subFilter)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query subscriptions", err))
|
||||
}
|
||||
|
||||
err = app.Database.Primary.IncUserMessageCounter(ctx, &user)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc user msg-counter", err))
|
||||
}
|
||||
|
||||
err = app.Database.Primary.IncChannelMessageCounter(ctx, &channel)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc channel msg-counter", err))
|
||||
}
|
||||
|
||||
err = app.Database.Primary.IncKeyTokenMessageCounter(ctx, keytok)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc token msg-counter", err))
|
||||
}
|
||||
|
||||
log.Info().Msg(fmt.Sprintf("Sending new notification %s for user %s", msg.MessageID, UserID))
|
||||
|
||||
for _, sub := range activeSubscriptions {
|
||||
clients, err := app.Database.Primary.ListClients(ctx, sub.SubscriberUserID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query clients", err))
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
|
||||
isCompatClient, err := app.Database.Primary.IsCompatClient(ctx, client.ClientID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query compat_clients", err))
|
||||
}
|
||||
|
||||
var titleOverride *string = nil
|
||||
var msgidOverride *string = nil
|
||||
if isCompatClient {
|
||||
titleOverride = langext.Ptr(app.CompatizeMessageTitle(ctx, msg))
|
||||
msgidOverride = langext.Ptr(fmt.Sprintf("%d", compatMsgID))
|
||||
}
|
||||
|
||||
fcmDelivID, err := app.DeliverMessage(ctx, client, msg, titleOverride, msgidOverride)
|
||||
if err != nil {
|
||||
_, err = app.Database.Primary.CreateRetryDelivery(ctx, client, msg)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create delivery", err))
|
||||
}
|
||||
} else {
|
||||
_, err = app.Database.Primary.CreateSuccessDelivery(ctx, client, msg, fcmDelivID)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create delivery", err))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return &SendMessageResponse{
|
||||
User: user,
|
||||
Message: msg,
|
||||
MessageIsOld: false,
|
||||
CompatMessageID: compatMsgID,
|
||||
}, nil
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
)
|
||||
|
||||
|
@ -43,7 +44,7 @@ func (ac *AppContext) CheckPermissionChanMessagesRead(channel models.Channel) *g
|
|||
return nil // owned channel
|
||||
} else {
|
||||
sub, err := ac.app.Database.Primary.GetSubscriptionBySubscriber(ac, p.Token.OwnerUserID, channel.ChannelID)
|
||||
if err == sql.ErrNoRows {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -97,6 +98,15 @@ func (ac *AppContext) CheckPermissionMessageRead(msg models.Message) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (ac *AppContext) CheckPermissionMessageDelete(msg models.Message) bool {
|
||||
p := ac.permissions
|
||||
if p.Token != nil && p.Token.IsAdmin(msg.SenderUserID) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ac *AppContext) CheckPermissionAny() *ginresp.HTTPResponse {
|
||||
p := ac.permissions
|
||||
if p.Token == nil {
|
||||
|
|
|
@ -4,7 +4,7 @@ package models
|
|||
|
||||
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
|
||||
const ChecksumGenerator = "4bfd61daa179e1452035a34c25c6f8170a08500bc0a7aa0e3981f95ad4b0d7d2"
|
||||
const ChecksumGenerator = "38908fc9adc16eb3a1266e4bca06e50ebc8613c5d3c9a4fea39314115f66544e"
|
||||
|
||||
type Enum interface {
|
||||
Valid() bool
|
||||
|
@ -59,10 +59,7 @@ func (e ClientType) ValuesAny() []any {
|
|||
}
|
||||
|
||||
func (e ClientType) ValuesMeta() []EnumMetaValue {
|
||||
return []EnumMetaValue{
|
||||
EnumMetaValue{VarName: "ClientTypeAndroid", Value: ClientTypeAndroid, Description: nil},
|
||||
EnumMetaValue{VarName: "ClientTypeIOS", Value: ClientTypeIOS, Description: nil},
|
||||
}
|
||||
return ClientTypeValuesMeta()
|
||||
}
|
||||
|
||||
func (e ClientType) String() string {
|
||||
|
@ -76,6 +73,10 @@ func (e ClientType) VarName() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (e ClientType) Meta() EnumMetaValue {
|
||||
return EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
|
||||
}
|
||||
|
||||
func ParseClientType(vv string) (ClientType, bool) {
|
||||
for _, ev := range __ClientTypeValues {
|
||||
if string(ev) == vv {
|
||||
|
@ -91,8 +92,8 @@ func ClientTypeValues() []ClientType {
|
|||
|
||||
func ClientTypeValuesMeta() []EnumMetaValue {
|
||||
return []EnumMetaValue{
|
||||
EnumMetaValue{VarName: "ClientTypeAndroid", Value: ClientTypeAndroid, Description: nil},
|
||||
EnumMetaValue{VarName: "ClientTypeIOS", Value: ClientTypeIOS, Description: nil},
|
||||
ClientTypeAndroid.Meta(),
|
||||
ClientTypeIOS.Meta(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,11 +129,7 @@ func (e DeliveryStatus) ValuesAny() []any {
|
|||
}
|
||||
|
||||
func (e DeliveryStatus) ValuesMeta() []EnumMetaValue {
|
||||
return []EnumMetaValue{
|
||||
EnumMetaValue{VarName: "DeliveryStatusRetry", Value: DeliveryStatusRetry, Description: nil},
|
||||
EnumMetaValue{VarName: "DeliveryStatusSuccess", Value: DeliveryStatusSuccess, Description: nil},
|
||||
EnumMetaValue{VarName: "DeliveryStatusFailed", Value: DeliveryStatusFailed, Description: nil},
|
||||
}
|
||||
return DeliveryStatusValuesMeta()
|
||||
}
|
||||
|
||||
func (e DeliveryStatus) String() string {
|
||||
|
@ -146,6 +143,10 @@ func (e DeliveryStatus) VarName() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (e DeliveryStatus) Meta() EnumMetaValue {
|
||||
return EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
|
||||
}
|
||||
|
||||
func ParseDeliveryStatus(vv string) (DeliveryStatus, bool) {
|
||||
for _, ev := range __DeliveryStatusValues {
|
||||
if string(ev) == vv {
|
||||
|
@ -161,9 +162,9 @@ func DeliveryStatusValues() []DeliveryStatus {
|
|||
|
||||
func DeliveryStatusValuesMeta() []EnumMetaValue {
|
||||
return []EnumMetaValue{
|
||||
EnumMetaValue{VarName: "DeliveryStatusRetry", Value: DeliveryStatusRetry, Description: nil},
|
||||
EnumMetaValue{VarName: "DeliveryStatusSuccess", Value: DeliveryStatusSuccess, Description: nil},
|
||||
EnumMetaValue{VarName: "DeliveryStatusFailed", Value: DeliveryStatusFailed, Description: nil},
|
||||
DeliveryStatusRetry.Meta(),
|
||||
DeliveryStatusSuccess.Meta(),
|
||||
DeliveryStatusFailed.Meta(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,12 +209,7 @@ func (e TokenPerm) ValuesAny() []any {
|
|||
}
|
||||
|
||||
func (e TokenPerm) ValuesMeta() []EnumMetaValue {
|
||||
return []EnumMetaValue{
|
||||
EnumMetaValue{VarName: "PermAdmin", Value: PermAdmin, Description: langext.Ptr("Edit userdata (+ includes all other permissions)")},
|
||||
EnumMetaValue{VarName: "PermChannelRead", Value: PermChannelRead, Description: langext.Ptr("Read messages")},
|
||||
EnumMetaValue{VarName: "PermChannelSend", Value: PermChannelSend, Description: langext.Ptr("Send messages")},
|
||||
EnumMetaValue{VarName: "PermUserRead", Value: PermUserRead, Description: langext.Ptr("Read userdata")},
|
||||
}
|
||||
return TokenPermValuesMeta()
|
||||
}
|
||||
|
||||
func (e TokenPerm) String() string {
|
||||
|
@ -234,6 +230,10 @@ func (e TokenPerm) VarName() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (e TokenPerm) Meta() EnumMetaValue {
|
||||
return EnumMetaValue{VarName: e.VarName(), Value: e, Description: langext.Ptr(e.Description())}
|
||||
}
|
||||
|
||||
func ParseTokenPerm(vv string) (TokenPerm, bool) {
|
||||
for _, ev := range __TokenPermValues {
|
||||
if string(ev) == vv {
|
||||
|
@ -249,9 +249,9 @@ func TokenPermValues() []TokenPerm {
|
|||
|
||||
func TokenPermValuesMeta() []EnumMetaValue {
|
||||
return []EnumMetaValue{
|
||||
EnumMetaValue{VarName: "PermAdmin", Value: PermAdmin, Description: langext.Ptr("Edit userdata (+ includes all other permissions)")},
|
||||
EnumMetaValue{VarName: "PermChannelRead", Value: PermChannelRead, Description: langext.Ptr("Read messages")},
|
||||
EnumMetaValue{VarName: "PermChannelSend", Value: PermChannelSend, Description: langext.Ptr("Send messages")},
|
||||
EnumMetaValue{VarName: "PermUserRead", Value: PermUserRead, Description: langext.Ptr("Read userdata")},
|
||||
PermAdmin.Meta(),
|
||||
PermChannelRead.Meta(),
|
||||
PermChannelSend.Meta(),
|
||||
PermUserRead.Meta(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,7 @@ const (
|
|||
|
||||
type Message struct {
|
||||
MessageID MessageID
|
||||
SenderUserID UserID // user that sent the message
|
||||
OwnerUserID UserID // oner of the message (= owner of the channel that contains it)
|
||||
SenderUserID UserID // user that sent the message (this is also the owner of the channel that contains it)
|
||||
ChannelInternalName string
|
||||
ChannelID ChannelID
|
||||
SenderName *string
|
||||
|
@ -34,7 +33,6 @@ func (m Message) FullJSON() MessageJSON {
|
|||
return MessageJSON{
|
||||
MessageID: m.MessageID,
|
||||
SenderUserID: m.SenderUserID,
|
||||
OwnerUserID: m.OwnerUserID,
|
||||
ChannelInternalName: m.ChannelInternalName,
|
||||
ChannelID: m.ChannelID,
|
||||
SenderName: m.SenderName,
|
||||
|
@ -53,7 +51,6 @@ func (m Message) TrimmedJSON() MessageJSON {
|
|||
return MessageJSON{
|
||||
MessageID: m.MessageID,
|
||||
SenderUserID: m.SenderUserID,
|
||||
OwnerUserID: m.OwnerUserID,
|
||||
ChannelInternalName: m.ChannelInternalName,
|
||||
ChannelID: m.ChannelID,
|
||||
SenderName: m.SenderName,
|
||||
|
@ -99,7 +96,6 @@ func (m Message) ShortContent() string {
|
|||
type MessageJSON struct {
|
||||
MessageID MessageID `json:"message_id"`
|
||||
SenderUserID UserID `json:"sender_user_id"`
|
||||
OwnerUserID UserID `json:"owner_user_id"`
|
||||
ChannelInternalName string `json:"channel_internal_name"`
|
||||
ChannelID ChannelID `json:"channel_id"`
|
||||
SenderName *string `json:"sender_name"`
|
||||
|
@ -116,7 +112,6 @@ type MessageJSON struct {
|
|||
type MessageDB struct {
|
||||
MessageID MessageID `db:"message_id"`
|
||||
SenderUserID UserID `db:"sender_user_id"`
|
||||
OwnerUserID UserID `db:"owner_user_id"`
|
||||
ChannelInternalName string `db:"channel_internal_name"`
|
||||
ChannelID ChannelID `db:"channel_id"`
|
||||
SenderName *string `db:"sender_name"`
|
||||
|
@ -135,7 +130,6 @@ func (m MessageDB) Model() Message {
|
|||
return Message{
|
||||
MessageID: m.MessageID,
|
||||
SenderUserID: m.SenderUserID,
|
||||
OwnerUserID: m.OwnerUserID,
|
||||
ChannelInternalName: m.ChannelInternalName,
|
||||
ChannelID: m.ChannelID,
|
||||
SenderName: m.SenderName,
|
||||
|
|
|
@ -17,7 +17,6 @@ type MessageFilter struct {
|
|||
ConfirmedSubscriptionBy *UserID
|
||||
SearchString *[]string
|
||||
Sender *[]UserID
|
||||
Owner *[]UserID
|
||||
ChannelNameCS *[]string // case-sensitive
|
||||
ChannelNameCI *[]string // case-insensitive
|
||||
ChannelID *[]ChannelID
|
||||
|
@ -79,15 +78,6 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
|
|||
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
|
||||
}
|
||||
|
||||
if f.Owner != nil {
|
||||
filter := make([]string, 0)
|
||||
for i, v := range *f.Owner {
|
||||
filter = append(filter, fmt.Sprintf("(owner_user_id = :owner_%d)", i))
|
||||
params[fmt.Sprintf("owner_%d", i)] = v
|
||||
}
|
||||
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
|
||||
}
|
||||
|
||||
if f.ChannelNameCI != nil {
|
||||
filter := make([]string, 0)
|
||||
for i, v := range *f.ChannelNameCI {
|
||||
|
@ -169,7 +159,7 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
|
|||
|
||||
if f.TimestampRealBefore != nil {
|
||||
sqlClauses = append(sqlClauses, "(timestamp_real < :ts_real_before)")
|
||||
params["ts_real_before"] = (*f.TimestampRealAfter).UnixMilli()
|
||||
params["ts_real_before"] = (*f.TimestampRealBefore).UnixMilli()
|
||||
}
|
||||
|
||||
if f.TimestampClient != nil {
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SubscriptionFilter struct {
|
||||
AnyUserID *UserID
|
||||
SubscriberUserID *[]UserID
|
||||
SubscriberUserID2 *[]UserID // Used to filter <SubscriberUserID> again
|
||||
ChannelOwnerUserID *[]UserID
|
||||
ChannelOwnerUserID2 *[]UserID // Used to filter <ChannelOwnerUserID> again
|
||||
ChannelID *[]ChannelID
|
||||
Confirmed *bool
|
||||
SubscriberIsChannelOwner *bool
|
||||
Timestamp *time.Time
|
||||
TimestampAfter *time.Time
|
||||
TimestampBefore *time.Time
|
||||
}
|
||||
|
||||
func (f SubscriptionFilter) SQL() (string, string, sq.PP, error) {
|
||||
|
||||
joinClause := ""
|
||||
|
||||
sqlClauses := make([]string, 0)
|
||||
|
||||
params := sq.PP{}
|
||||
|
||||
if f.AnyUserID != nil {
|
||||
sqlClauses = append(sqlClauses, "(subscriber_user_id = :anyuid1 OR channel_owner_user_id = :anyuid2)")
|
||||
params["anyuid1"] = *f.AnyUserID
|
||||
params["anyuid2"] = *f.AnyUserID
|
||||
}
|
||||
|
||||
if f.SubscriberUserID != nil {
|
||||
filter := make([]string, 0)
|
||||
for i, v := range *f.SubscriberUserID {
|
||||
filter = append(filter, fmt.Sprintf("(subscriber_user_id = :subscriber_uid_1_%d)", i))
|
||||
params[fmt.Sprintf("subscriber_uid_1_%d", i)] = v
|
||||
}
|
||||
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
|
||||
}
|
||||
|
||||
if f.SubscriberUserID2 != nil {
|
||||
filter := make([]string, 0)
|
||||
for i, v := range *f.SubscriberUserID2 {
|
||||
filter = append(filter, fmt.Sprintf("(subscriber_user_id = :subscriber_uid_2_%d)", i))
|
||||
params[fmt.Sprintf("subscriber_uid_2_%d", i)] = v
|
||||
}
|
||||
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
|
||||
}
|
||||
|
||||
if f.ChannelOwnerUserID != nil {
|
||||
filter := make([]string, 0)
|
||||
for i, v := range *f.ChannelOwnerUserID {
|
||||
filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :chanowner_uid_1_%d)", i))
|
||||
params[fmt.Sprintf("chanowner_uid_1_%d", i)] = v
|
||||
}
|
||||
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
|
||||
}
|
||||
|
||||
if f.ChannelOwnerUserID2 != nil {
|
||||
filter := make([]string, 0)
|
||||
for i, v := range *f.ChannelOwnerUserID2 {
|
||||
filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :chanowner_uid_2_%d)", i))
|
||||
params[fmt.Sprintf("chanowner_uid_2_%d", i)] = v
|
||||
}
|
||||
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
|
||||
}
|
||||
|
||||
if f.ChannelID != nil {
|
||||
filter := make([]string, 0)
|
||||
for i, v := range *f.ChannelID {
|
||||
filter = append(filter, fmt.Sprintf("(channel_id = :chanid_%d)", i))
|
||||
params[fmt.Sprintf("chanid_%d", i)] = v
|
||||
}
|
||||
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
|
||||
}
|
||||
|
||||
if f.Confirmed != nil {
|
||||
if *f.Confirmed {
|
||||
sqlClauses = append(sqlClauses, "(confirmed=1)")
|
||||
} else {
|
||||
sqlClauses = append(sqlClauses, "(confirmed=0)")
|
||||
}
|
||||
}
|
||||
|
||||
if f.SubscriberIsChannelOwner != nil {
|
||||
if *f.SubscriberIsChannelOwner {
|
||||
sqlClauses = append(sqlClauses, "(subscriber_user_id = channel_owner_user_id)")
|
||||
} else {
|
||||
sqlClauses = append(sqlClauses, "(subscriber_user_id != channel_owner_user_id)")
|
||||
}
|
||||
}
|
||||
|
||||
if f.Timestamp != nil {
|
||||
sqlClauses = append(sqlClauses, "(timestamp_created = :ts_equals)")
|
||||
params["ts_equals"] = (*f.Timestamp).UnixMilli()
|
||||
}
|
||||
|
||||
if f.TimestampAfter != nil {
|
||||
sqlClauses = append(sqlClauses, "(timestamp_created > :ts_after)")
|
||||
params["ts_after"] = (*f.TimestampAfter).UnixMilli()
|
||||
}
|
||||
|
||||
if f.TimestampBefore != nil {
|
||||
sqlClauses = append(sqlClauses, "(timestamp_created < :ts_before)")
|
||||
params["ts_before"] = (*f.TimestampBefore).UnixMilli()
|
||||
}
|
||||
|
||||
sqlClause := ""
|
||||
if len(sqlClauses) > 0 {
|
||||
sqlClause = strings.Join(sqlClauses, " AND ")
|
||||
} else {
|
||||
sqlClause = "1=1"
|
||||
}
|
||||
|
||||
return sqlClause, joinClause, params, nil
|
||||
}
|
||||
|
||||
func (f SubscriptionFilter) Hash() string {
|
||||
bh, err := dataext.StructHash(f, dataext.StructHashOptions{HashAlgo: sha512.New()})
|
||||
if err != nil {
|
||||
return "00000000"
|
||||
}
|
||||
|
||||
str := hex.EncodeToString(bh)
|
||||
return str[0:mathext.Min(8, len(bh))]
|
||||
}
|
|
@ -23,17 +23,24 @@ type User struct {
|
|||
|
||||
func (u User) JSON() UserJSON {
|
||||
return UserJSON{
|
||||
UserID: u.UserID,
|
||||
Username: u.Username,
|
||||
TimestampCreated: u.TimestampCreated.Format(time.RFC3339Nano),
|
||||
TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano),
|
||||
TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano),
|
||||
MessagesSent: u.MessagesSent,
|
||||
QuotaUsed: u.QuotaUsedToday(),
|
||||
QuotaPerDay: u.QuotaPerDay(),
|
||||
QuotaRemaining: u.QuotaRemainingToday(),
|
||||
IsPro: u.IsPro,
|
||||
DefaultChannel: u.DefaultChannel(),
|
||||
UserID: u.UserID,
|
||||
Username: u.Username,
|
||||
TimestampCreated: u.TimestampCreated.Format(time.RFC3339Nano),
|
||||
TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano),
|
||||
TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano),
|
||||
MessagesSent: u.MessagesSent,
|
||||
QuotaUsed: u.QuotaUsedToday(),
|
||||
QuotaPerDay: u.QuotaPerDay(),
|
||||
QuotaRemaining: u.QuotaRemainingToday(),
|
||||
IsPro: u.IsPro,
|
||||
DefaultChannel: u.DefaultChannel(),
|
||||
MaxBodySize: u.MaxContentLength(),
|
||||
MaxTitleLength: u.MaxTitleLength(),
|
||||
DefaultPriority: u.DefaultPriority(),
|
||||
MaxChannelNameLength: u.MaxChannelNameLength(),
|
||||
MaxChannelDescriptionLength: u.MaxChannelDescriptionLength(),
|
||||
MaxSenderNameLength: u.MaxSenderNameLength(),
|
||||
MaxUserMessageIDLength: u.MaxUserMessageIDLength(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,9 +56,9 @@ func (u User) JSONWithClients(clients []Client, ak string, sk string, rk string)
|
|||
|
||||
func (u User) MaxContentLength() int {
|
||||
if u.IsPro {
|
||||
return 16384
|
||||
return 2 * 1024 * 1024 // 2 MB
|
||||
} else {
|
||||
return 2048
|
||||
return 2 * 1024 // 2 KB
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,7 +68,7 @@ func (u User) MaxTitleLength() int {
|
|||
|
||||
func (u User) QuotaPerDay() int {
|
||||
if u.IsPro {
|
||||
return 1000
|
||||
return 5000
|
||||
} else {
|
||||
return 50
|
||||
}
|
||||
|
@ -92,15 +99,15 @@ func (u User) MaxChannelNameLength() int {
|
|||
return 120
|
||||
}
|
||||
|
||||
func (u User) MaxChannelDescriptionNameLength() int {
|
||||
func (u User) MaxChannelDescriptionLength() int {
|
||||
return 300
|
||||
}
|
||||
|
||||
func (u User) MaxSenderName() int {
|
||||
func (u User) MaxSenderNameLength() int {
|
||||
return 120
|
||||
}
|
||||
|
||||
func (u User) MaxUserMessageID() int {
|
||||
func (u User) MaxUserMessageIDLength() int {
|
||||
return 64
|
||||
}
|
||||
|
||||
|
@ -109,17 +116,24 @@ func (u User) MaxTimestampDiffHours() int {
|
|||
}
|
||||
|
||||
type UserJSON struct {
|
||||
UserID UserID `json:"user_id"`
|
||||
Username *string `json:"username"`
|
||||
TimestampCreated string `json:"timestamp_created"`
|
||||
TimestampLastRead *string `json:"timestamp_lastread"`
|
||||
TimestampLastSent *string `json:"timestamp_lastsent"`
|
||||
MessagesSent int `json:"messages_sent"`
|
||||
QuotaUsed int `json:"quota_used"`
|
||||
QuotaRemaining int `json:"quota_remaining"`
|
||||
QuotaPerDay int `json:"quota_max"`
|
||||
IsPro bool `json:"is_pro"`
|
||||
DefaultChannel string `json:"default_channel"`
|
||||
UserID UserID `json:"user_id"`
|
||||
Username *string `json:"username"`
|
||||
TimestampCreated string `json:"timestamp_created"`
|
||||
TimestampLastRead *string `json:"timestamp_lastread"`
|
||||
TimestampLastSent *string `json:"timestamp_lastsent"`
|
||||
MessagesSent int `json:"messages_sent"`
|
||||
QuotaUsed int `json:"quota_used"`
|
||||
QuotaRemaining int `json:"quota_remaining"`
|
||||
QuotaPerDay int `json:"quota_max"`
|
||||
IsPro bool `json:"is_pro"`
|
||||
DefaultChannel string `json:"default_channel"`
|
||||
MaxBodySize int `json:"max_body_size"`
|
||||
MaxTitleLength int `json:"max_title_length"`
|
||||
DefaultPriority int `json:"default_priority"`
|
||||
MaxChannelNameLength int `json:"max_channel_name_length"`
|
||||
MaxChannelDescriptionLength int `json:"max_channel_description_length"`
|
||||
MaxSenderNameLength int `json:"max_sender_name_length"`
|
||||
MaxUserMessageIDLength int `json:"max_user_message_id_length"`
|
||||
}
|
||||
|
||||
type UserJSONWithClientsAndKeys struct {
|
||||
|
|
|
@ -2160,7 +2160,7 @@
|
|||
},
|
||||
"/api/v2/users/{uid}/subscriptions": {
|
||||
"get": {
|
||||
"description": "The possible values for 'selector' are:\n- \"outgoing_all\" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)\n- \"outgoing_confirmed\" Confirmed subscriptions with the user as subscriber\n- \"outgoing_unconfirmed\" Unconfirmed (Pending) subscriptions with the user as subscriber\n- \"incoming_all\" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)\n- \"incoming_confirmed\" Confirmed subscriptions from other users to channels of this user\n- \"incoming_unconfirmed\" Unconfirmed subscriptions from other users to channels of this user (= requests)",
|
||||
"description": "The possible values for 'direction' are:\n- \"outgoing\" Subscriptions with the user as subscriber (= subscriptions he can use to read channels)\n- \"incoming\" Subscriptions to channels of this user (= incoming subscriptions and subscription requests)\n- \"both\" Combines \"outgoing\" and \"incoming\" (default)\n\nThe possible values for 'confirmation' are:\n- \"confirmed\" Confirmed (active) subscriptions\n- \"unconfirmed\" Unconfirmed (pending) subscriptions\n- \"all\" Combines \"confirmed\" and \"unconfirmed\" (default)\n\nThe possible values for 'external' are:\n- \"true\" Subscriptions with subscriber_user_id != channel_owner_user_id (subscriptions from other users)\n- \"false\" Subscriptions with subscriber_user_id == channel_owner_user_id (subscriptions from this user to his own channels)\n- \"all\" Combines \"external\" and \"internal\" (default)\n\nThe `subscriber_user_id` parameter can be used to additionally filter the subscriber_user_id (return subscribtions from a specific user)\n\nThe `channel_owner_user_id` parameter can be used to additionally filter the channel_owner_user_id (return subscribtions to a specific user)",
|
||||
"tags": [
|
||||
"API-v2"
|
||||
],
|
||||
|
@ -2731,6 +2731,69 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/webhook/uptime-kuma": {
|
||||
"post": {
|
||||
"description": "All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required",
|
||||
"tags": [
|
||||
"External"
|
||||
],
|
||||
"summary": "Send a new message",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "P3TNH8mvv14fm",
|
||||
"name": "key",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "7725",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "post_body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.UptimeKumaWebHook.uptimeKumaWebhookBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "The user_id was not found or the user_key is wrong",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user has exceeded its daily quota - wait 24 hours or upgrade your account",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "An internal server error occurred - try again later",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
|
@ -2952,8 +3015,6 @@
|
|||
"handler.CreateUserKey.body": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"all_channels",
|
||||
"channels",
|
||||
"name",
|
||||
"permissions"
|
||||
],
|
||||
|
@ -3388,6 +3449,45 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"handler.UptimeKumaWebHook.uptimeKumaWebhookBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heartbeat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"localDateTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"time": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezoneOffset": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"monitor": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handler.pingResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -3574,9 +3674,6 @@
|
|||
"message_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner_user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
@ -3638,9 +3735,30 @@
|
|||
"default_channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"default_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_pro": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_body_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_channel_description_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_channel_name_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_sender_name_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_title_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_user_message_id_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"messages_sent": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
@ -3685,9 +3803,30 @@
|
|||
"default_channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"default_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_pro": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_body_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_channel_description_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_channel_name_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_sender_name_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_title_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_user_message_id_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"messages_sent": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
|
|
@ -183,8 +183,6 @@ definitions:
|
|||
permissions:
|
||||
type: string
|
||||
required:
|
||||
- all_channels
|
||||
- channels
|
||||
- name
|
||||
- permissions
|
||||
type: object
|
||||
|
@ -460,6 +458,31 @@ definitions:
|
|||
user_id:
|
||||
type: integer
|
||||
type: object
|
||||
handler.UptimeKumaWebHook.uptimeKumaWebhookBody:
|
||||
properties:
|
||||
heartbeat:
|
||||
properties:
|
||||
localDateTime:
|
||||
type: string
|
||||
msg:
|
||||
type: string
|
||||
time:
|
||||
type: string
|
||||
timezone:
|
||||
type: string
|
||||
timezoneOffset:
|
||||
type: string
|
||||
type: object
|
||||
monitor:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
msg:
|
||||
type: string
|
||||
type: object
|
||||
handler.pingResponse:
|
||||
properties:
|
||||
info:
|
||||
|
@ -583,8 +606,6 @@ definitions:
|
|||
type: string
|
||||
message_id:
|
||||
type: string
|
||||
owner_user_id:
|
||||
type: string
|
||||
priority:
|
||||
type: integer
|
||||
sender_ip:
|
||||
|
@ -625,8 +646,22 @@ definitions:
|
|||
properties:
|
||||
default_channel:
|
||||
type: string
|
||||
default_priority:
|
||||
type: integer
|
||||
is_pro:
|
||||
type: boolean
|
||||
max_body_size:
|
||||
type: integer
|
||||
max_channel_description_length:
|
||||
type: integer
|
||||
max_channel_name_length:
|
||||
type: integer
|
||||
max_sender_name_length:
|
||||
type: integer
|
||||
max_title_length:
|
||||
type: integer
|
||||
max_user_message_id_length:
|
||||
type: integer
|
||||
messages_sent:
|
||||
type: integer
|
||||
quota_max:
|
||||
|
@ -656,8 +691,22 @@ definitions:
|
|||
type: array
|
||||
default_channel:
|
||||
type: string
|
||||
default_priority:
|
||||
type: integer
|
||||
is_pro:
|
||||
type: boolean
|
||||
max_body_size:
|
||||
type: integer
|
||||
max_channel_description_length:
|
||||
type: integer
|
||||
max_channel_name_length:
|
||||
type: integer
|
||||
max_sender_name_length:
|
||||
type: integer
|
||||
max_title_length:
|
||||
type: integer
|
||||
max_user_message_id_length:
|
||||
type: integer
|
||||
messages_sent:
|
||||
type: integer
|
||||
quota_max:
|
||||
|
@ -2153,13 +2202,24 @@ paths:
|
|||
/api/v2/users/{uid}/subscriptions:
|
||||
get:
|
||||
description: |-
|
||||
The possible values for 'selector' are:
|
||||
- "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)
|
||||
- "outgoing_confirmed" Confirmed subscriptions with the user as subscriber
|
||||
- "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber
|
||||
- "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
|
||||
- "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
|
||||
- "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
|
||||
The possible values for 'direction' are:
|
||||
- "outgoing" Subscriptions with the user as subscriber (= subscriptions he can use to read channels)
|
||||
- "incoming" Subscriptions to channels of this user (= incoming subscriptions and subscription requests)
|
||||
- "both" Combines "outgoing" and "incoming" (default)
|
||||
|
||||
The possible values for 'confirmation' are:
|
||||
- "confirmed" Confirmed (active) subscriptions
|
||||
- "unconfirmed" Unconfirmed (pending) subscriptions
|
||||
- "all" Combines "confirmed" and "unconfirmed" (default)
|
||||
|
||||
The possible values for 'external' are:
|
||||
- "true" Subscriptions with subscriber_user_id != channel_owner_user_id (subscriptions from other users)
|
||||
- "false" Subscriptions with subscriber_user_id == channel_owner_user_id (subscriptions from this user to his own channels)
|
||||
- "all" Combines "external" and "internal" (default)
|
||||
|
||||
The `subscriber_user_id` parameter can be used to additionally filter the subscriber_user_id (return subscribtions from a specific user)
|
||||
|
||||
The `channel_owner_user_id` parameter can be used to additionally filter the channel_owner_user_id (return subscribtions to a specific user)
|
||||
operationId: api-user-subscriptions-list
|
||||
parameters:
|
||||
- description: UserID
|
||||
|
@ -2541,6 +2601,49 @@ paths:
|
|||
summary: Send a new message (compatibility)
|
||||
tags:
|
||||
- External
|
||||
/webhook/uptime-kuma:
|
||||
post:
|
||||
description: All parameter can be set via query-parameter or the json body.
|
||||
Only UserID, UserKey and Title are required
|
||||
parameters:
|
||||
- example: P3TNH8mvv14fm
|
||||
in: query
|
||||
name: key
|
||||
type: string
|
||||
- example: "7725"
|
||||
in: query
|
||||
name: user_id
|
||||
type: string
|
||||
- description: ' '
|
||||
in: body
|
||||
name: post_body
|
||||
schema:
|
||||
$ref: '#/definitions/handler.UptimeKumaWebHook.uptimeKumaWebhookBody'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"401":
|
||||
description: The user_id was not found or the user_key is wrong
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"403":
|
||||
description: The user has exceeded its daily quota - wait 24 hours or upgrade
|
||||
your account
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"500":
|
||||
description: An internal server error occurred - try again later
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
summary: Send a new message
|
||||
tags:
|
||||
- External
|
||||
swagger: "2.0"
|
||||
tags:
|
||||
- name: External
|
||||
|
|
|
@ -54,6 +54,16 @@ func TestCreateChannel(t *testing.T) {
|
|||
tt.AssertMappedSet(t, "channels", []string{"asdf", "test"}, clist.Channels, "display_name")
|
||||
tt.AssertMappedSet(t, "channels", []string{"asdf", "test"}, clist.Channels, "internal_name")
|
||||
}
|
||||
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", uid), gin.H{
|
||||
"name": "withdesc",
|
||||
"description": "desc",
|
||||
})
|
||||
|
||||
{
|
||||
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", uid))
|
||||
tt.AssertEqual(t, "chan.len", 3, len(clist.Channels))
|
||||
tt.AssertEqual(t, "description_name", "desc", clist.Channels[2]["description_name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannelNameTooLong(t *testing.T) {
|
||||
|
@ -687,40 +697,40 @@ func TestListChannelSubscriptions(t *testing.T) {
|
|||
}
|
||||
|
||||
countBoth := func(oa1, oc1, ou1, ia1, ic1, iu1, oa2, oc2, ou2, ia2, ic2, iu2 int) {
|
||||
sublist1oa := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "outgoing_all"))
|
||||
sublist1oa := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "outgoing", "all"))
|
||||
tt.AssertEqual(t, "1:outgoing_all", oa1, len(sublist1oa.Subscriptions))
|
||||
|
||||
sublist1oc := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "outgoing_confirmed"))
|
||||
sublist1oc := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "outgoing", "confirmed"))
|
||||
tt.AssertEqual(t, "1:outgoing_confirmed", oc1, len(sublist1oc.Subscriptions))
|
||||
|
||||
sublist1ou := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "outgoing_unconfirmed"))
|
||||
sublist1ou := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "outgoing", "unconfirmed"))
|
||||
tt.AssertEqual(t, "1:outgoing_unconfirmed", ou1, len(sublist1ou.Subscriptions))
|
||||
|
||||
sublist1ia := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "incoming_all"))
|
||||
sublist1ia := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "incoming", "all"))
|
||||
tt.AssertEqual(t, "1:incoming_all", ia1, len(sublist1ia.Subscriptions))
|
||||
|
||||
sublist1ic := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "incoming_confirmed"))
|
||||
sublist1ic := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "incoming", "confirmed"))
|
||||
tt.AssertEqual(t, "1:incoming_confirmed", ic1, len(sublist1ic.Subscriptions))
|
||||
|
||||
sublist1iu := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "incoming_unconfirmed"))
|
||||
sublist1iu := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "incoming", "unconfirmed"))
|
||||
tt.AssertEqual(t, "1:incoming_unconfirmed", iu1, len(sublist1iu.Subscriptions))
|
||||
|
||||
sublist2oa := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "outgoing_all"))
|
||||
sublist2oa := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "outgoing", "all"))
|
||||
tt.AssertEqual(t, "2:outgoing_all", oa2, len(sublist2oa.Subscriptions))
|
||||
|
||||
sublist2oc := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "outgoing_confirmed"))
|
||||
sublist2oc := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "outgoing", "confirmed"))
|
||||
tt.AssertEqual(t, "2:outgoing_confirmed", oc2, len(sublist2oc.Subscriptions))
|
||||
|
||||
sublist2ou := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "outgoing_unconfirmed"))
|
||||
sublist2ou := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "outgoing", "unconfirmed"))
|
||||
tt.AssertEqual(t, "2:outgoing_unconfirmed", ou2, len(sublist2ou.Subscriptions))
|
||||
|
||||
sublist2ia := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "incoming_all"))
|
||||
sublist2ia := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "incoming", "all"))
|
||||
tt.AssertEqual(t, "2:incoming_all", ia2, len(sublist2ia.Subscriptions))
|
||||
|
||||
sublist2ic := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "incoming_confirmed"))
|
||||
sublist2ic := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "incoming", "confirmed"))
|
||||
tt.AssertEqual(t, "2:incoming_confirmed", ic2, len(sublist2ic.Subscriptions))
|
||||
|
||||
sublist2iu := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "incoming_unconfirmed"))
|
||||
sublist2iu := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "incoming", "unconfirmed"))
|
||||
tt.AssertEqual(t, "2:incoming_unconfirmed", iu2, len(sublist2iu.Subscriptions))
|
||||
}
|
||||
|
||||
|
@ -818,7 +828,7 @@ func TestListChannelSubscriptions(t *testing.T) {
|
|||
3, 3, 0,
|
||||
3, 3, 0)
|
||||
|
||||
sublistRem := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "incoming_confirmed"))
|
||||
sublistRem := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "incoming", "confirmed"))
|
||||
for _, v := range sublistRem.Subscriptions {
|
||||
tt.RequestAuthDelete[gin.H](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data2.UID, v.SubscriptionId), gin.H{})
|
||||
}
|
||||
|
|
|
@ -321,7 +321,7 @@ func TestCompatRegisterPro(t *testing.T) {
|
|||
tt.AssertEqual(t, "success", true, r0["success"])
|
||||
tt.AssertEqual(t, "message", "New user registered", r0["message"])
|
||||
tt.AssertEqual(t, "quota", 0, r0["quota"])
|
||||
tt.AssertEqual(t, "quota_max", 1000, r0["quota_max"])
|
||||
tt.AssertEqual(t, "quota_max", 5000, r0["quota_max"])
|
||||
tt.AssertEqual(t, "is_pro", true, r0["is_pro"])
|
||||
|
||||
r1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/register.php?fcm_token=%s&pro=%s&pro_token=%s", "DUMMY_FCM", "true", url.QueryEscape("INVALID")))
|
||||
|
@ -555,7 +555,7 @@ func TestCompatUpgrade(t *testing.T) {
|
|||
tt.AssertEqual(t, "success", true, r1["success"])
|
||||
tt.AssertEqual(t, "message", "user updated", r1["message"])
|
||||
tt.AssertEqual(t, "quota", 0, r1["quota"])
|
||||
tt.AssertEqual(t, "quota_max", 1000, r1["quota_max"])
|
||||
tt.AssertEqual(t, "quota_max", 5000, r1["quota_max"])
|
||||
tt.AssertEqual(t, "is_pro", true, r1["is_pro"])
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,388 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/db/impl/logs"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/impl/primary"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/impl/requests"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/schema"
|
||||
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
|
||||
tt "blackforestbytes.com/simplecloudnotifier/test/util"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrimaryDB_Current(t *testing.T) {
|
||||
dbf1, dbf2, dbf3, conf, stop := tt.StartSimpleTestspace(t)
|
||||
defer stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tt.AssertAny(dbf1)
|
||||
tt.AssertAny(dbf2)
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
{
|
||||
db1, err := primary.NewPrimaryDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", 0, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.PrimarySchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.PrimarySchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1.Stop(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
db1New, err := primary.NewPrimaryDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema3, err := db1New.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema3", schema.PrimarySchemaVersion, schema3)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1New.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema4, err := db1New.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema4", schema.PrimarySchemaVersion, schema4)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogsDB_Current(t *testing.T) {
|
||||
dbf1, dbf2, dbf3, conf, stop := tt.StartSimpleTestspace(t)
|
||||
defer stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tt.AssertAny(dbf1)
|
||||
tt.AssertAny(dbf2)
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
{
|
||||
db1, err := logs.NewLogsDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", 0, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.LogsSchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.LogsSchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1.Stop(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
db1New, err := logs.NewLogsDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema3, err := db1New.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema3", schema.LogsSchemaVersion, schema3)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1New.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema4, err := db1New.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema4", schema.LogsSchemaVersion, schema4)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestsDB_Current(t *testing.T) {
|
||||
dbf1, dbf2, dbf3, conf, stop := tt.StartSimpleTestspace(t)
|
||||
defer stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tt.AssertAny(dbf1)
|
||||
tt.AssertAny(dbf2)
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
{
|
||||
db1, err := requests.NewRequestsDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", 0, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.RequestsSchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.RequestsSchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1.Stop(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
db1New, err := requests.NewRequestsDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema3, err := db1New.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema3", schema.RequestsSchemaVersion, schema3)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1New.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema4, err := db1New.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema4", schema.RequestsSchemaVersion, schema4)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrimaryDB_Migrate_from_3(t *testing.T) {
|
||||
dbf1, dbf2, dbf3, conf, stop := tt.StartSimpleTestspace(t)
|
||||
defer stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tt.AssertAny(dbf1)
|
||||
tt.AssertAny(dbf2)
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
{
|
||||
url := fmt.Sprintf("file:%s", dbf1)
|
||||
|
||||
xdb, err := sqlx.Open("sqlite3", url)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
qqdb := sq.NewDB(xdb)
|
||||
|
||||
schemavers := 3
|
||||
|
||||
dbschema := schema.PrimarySchema[schemavers]
|
||||
|
||||
_, err = qqdb.Exec(ctx, dbschema.SQL, sq.PP{})
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
"key": "schema",
|
||||
"val": schemavers,
|
||||
})
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
"key": "schema_hash",
|
||||
"val": dbschema.Hash,
|
||||
})
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, qqdb)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", dbschema.Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = qqdb.Exit()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
db1, err := primary.NewPrimaryDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", 3, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.PrimarySchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, db1.DB())
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", schema.PrimarySchema[schema.PrimarySchemaVersion].Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1.Stop(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
@ -438,10 +439,13 @@ func TestSendLongContentPro(t *testing.T) {
|
|||
uid := r0["user_id"].(string)
|
||||
sendtok := r0["send_key"].(string)
|
||||
|
||||
str1k := strings.Repeat(".", 1000)
|
||||
str100k := strings.Repeat(":", 100_000)
|
||||
|
||||
{
|
||||
longContent := ""
|
||||
for i := 0; i < 400; i++ {
|
||||
longContent += "123456789\n" // 10 * 400 = 4_000 (max = 16_384)
|
||||
longContent += "123456789\n" // 10 * 400 = 4_000 (max = 2_097_152)
|
||||
}
|
||||
|
||||
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
|
||||
|
@ -455,7 +459,7 @@ func TestSendLongContentPro(t *testing.T) {
|
|||
{
|
||||
longContent := ""
|
||||
for i := 0; i < 800; i++ {
|
||||
longContent += "123456789\n" // 10 * 800 = 8_000 (max = 16_384)
|
||||
longContent += "123456789\n" // 10 * 800 = 8_000 (max = 2_097_152)
|
||||
}
|
||||
|
||||
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
|
||||
|
@ -469,8 +473,8 @@ func TestSendLongContentPro(t *testing.T) {
|
|||
|
||||
{
|
||||
longContent := ""
|
||||
for i := 0; i < 1600; i++ {
|
||||
longContent += "123456789\n" // 10 * 1600 = 16_000 (max = 16_384)
|
||||
for i := 0; i < 16; i++ {
|
||||
longContent += str1k // 16 * 1000 = 16_000 (max = 2_097_152)
|
||||
}
|
||||
|
||||
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
|
||||
|
@ -483,22 +487,8 @@ func TestSendLongContentPro(t *testing.T) {
|
|||
|
||||
{
|
||||
longContent := ""
|
||||
for i := 0; i < 1630; i++ {
|
||||
longContent += "123456789\n" // 10 * 1630 = 163_000 (max = 16_384)
|
||||
}
|
||||
|
||||
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
|
||||
"key": sendtok,
|
||||
"user_id": uid,
|
||||
"title": "HelloWorld_042",
|
||||
"content": longContent,
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
longContent := ""
|
||||
for i := 0; i < 1640; i++ {
|
||||
longContent += "123456789\n" // 10 * 1640 = 164_000 (max = 16_384)
|
||||
for i := 0; i < 21; i++ {
|
||||
longContent += str100k // 21 * 200_000 = 2_100_000 (max = 2_097_152)
|
||||
}
|
||||
|
||||
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
|
||||
|
@ -1276,8 +1266,8 @@ func TestQuotaExceededPro(t *testing.T) {
|
|||
sendtok := r0["send_key"].(string)
|
||||
|
||||
tt.AssertStrRepEqual(t, "quota.0", 0, r0["quota_used"])
|
||||
tt.AssertStrRepEqual(t, "quota.0", 1000, r0["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.0", 1000, r0["quota_remaining"])
|
||||
tt.AssertStrRepEqual(t, "quota.0", 5000, r0["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.0", 5000, r0["quota_remaining"])
|
||||
|
||||
{
|
||||
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
|
@ -1286,18 +1276,18 @@ func TestQuotaExceededPro(t *testing.T) {
|
|||
"title": tt.ShortLipsum0(2),
|
||||
})
|
||||
tt.AssertStrRepEqual(t, "quota.msg.1", 1, msg1["quota"])
|
||||
tt.AssertStrRepEqual(t, "quota.msg.1", 1000, msg1["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.msg.1", 5000, msg1["quota_max"])
|
||||
}
|
||||
|
||||
{
|
||||
usr := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s", uid))
|
||||
|
||||
tt.AssertStrRepEqual(t, "quota.1", 1, usr["quota_used"])
|
||||
tt.AssertStrRepEqual(t, "quota.1", 1000, usr["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.1", 999, usr["quota_remaining"])
|
||||
tt.AssertStrRepEqual(t, "quota.1", 5000, usr["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.1", 4999, usr["quota_remaining"])
|
||||
}
|
||||
|
||||
for i := 0; i < 998; i++ {
|
||||
for i := 0; i < 4998; i++ {
|
||||
|
||||
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
"key": sendtok,
|
||||
|
@ -1309,8 +1299,8 @@ func TestQuotaExceededPro(t *testing.T) {
|
|||
{
|
||||
usr := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s", uid))
|
||||
|
||||
tt.AssertStrRepEqual(t, "quota.999", 999, usr["quota_used"])
|
||||
tt.AssertStrRepEqual(t, "quota.999", 1000, usr["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.999", 4999, usr["quota_used"])
|
||||
tt.AssertStrRepEqual(t, "quota.999", 5000, usr["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.999", 1, usr["quota_remaining"])
|
||||
}
|
||||
|
||||
|
@ -1319,15 +1309,15 @@ func TestQuotaExceededPro(t *testing.T) {
|
|||
"user_id": uid,
|
||||
"title": tt.ShortLipsum0(2),
|
||||
})
|
||||
tt.AssertStrRepEqual(t, "quota.msg.1000", 1000, msg50["quota"])
|
||||
tt.AssertStrRepEqual(t, "quota.msg.1000", 1000, msg50["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.msg.5000", 5000, msg50["quota"])
|
||||
tt.AssertStrRepEqual(t, "quota.msg.5000", 5000, msg50["quota_max"])
|
||||
|
||||
{
|
||||
usr := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s", uid))
|
||||
|
||||
tt.AssertStrRepEqual(t, "quota.1000", 1000, usr["quota_used"])
|
||||
tt.AssertStrRepEqual(t, "quota.1000", 1000, usr["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.1000", 0, usr["quota_remaining"])
|
||||
tt.AssertStrRepEqual(t, "quota.5000", 5000, usr["quota_used"])
|
||||
tt.AssertStrRepEqual(t, "quota.5000", 5000, usr["quota_max"])
|
||||
tt.AssertStrRepEqual(t, "quota.5000", 0, usr["quota_remaining"])
|
||||
}
|
||||
|
||||
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
|
||||
|
|
|
@ -51,17 +51,36 @@ func TestListSubscriptionsOfUser(t *testing.T) {
|
|||
Channels []chanobj `json:"channels"`
|
||||
}
|
||||
|
||||
assertCount := func(u tt.Userdat, c int, sel string) {
|
||||
slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", u.UID, sel))
|
||||
tt.AssertEqual(t, sel+".len", c, len(slist.Subscriptions))
|
||||
assertCount := func(u tt.Userdat, c int, dir string, conf string) {
|
||||
slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", u.UID, dir, conf))
|
||||
tt.AssertEqual(t, dir+"."+conf+".len", c, len(slist.Subscriptions))
|
||||
}
|
||||
|
||||
assertCount(data.User[16], 3, "outgoing_all")
|
||||
assertCount(data.User[16], 3, "outgoing_confirmed")
|
||||
assertCount(data.User[16], 0, "outgoing_unconfirmed")
|
||||
assertCount(data.User[16], 3, "incoming_all")
|
||||
assertCount(data.User[16], 3, "incoming_confirmed")
|
||||
assertCount(data.User[16], 0, "incoming_unconfirmed")
|
||||
assertCount2 := func(u tt.Userdat, c int, dir string, conf string, ext string) {
|
||||
slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s&external=%s", u.UID, dir, conf, ext))
|
||||
tt.AssertEqual(t, dir+"."+conf+"."+ext+".len", c, len(slist.Subscriptions))
|
||||
}
|
||||
|
||||
assertCount(data.User[16], 3, "outgoing", "all")
|
||||
assertCount(data.User[16], 3, "outgoing", "confirmed")
|
||||
assertCount(data.User[16], 0, "outgoing", "unconfirmed")
|
||||
assertCount(data.User[16], 3, "incoming", "all")
|
||||
assertCount(data.User[16], 3, "incoming", "confirmed")
|
||||
assertCount(data.User[16], 0, "incoming", "unconfirmed")
|
||||
|
||||
assertCount2(data.User[16], 0, "outgoing", "all", "true")
|
||||
assertCount2(data.User[16], 0, "outgoing", "confirmed", "true")
|
||||
assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "true")
|
||||
assertCount2(data.User[16], 0, "incoming", "all", "true")
|
||||
assertCount2(data.User[16], 0, "incoming", "confirmed", "true")
|
||||
assertCount2(data.User[16], 0, "incoming", "unconfirmed", "true")
|
||||
|
||||
assertCount2(data.User[16], 3, "outgoing", "all", "false")
|
||||
assertCount2(data.User[16], 3, "outgoing", "confirmed", "false")
|
||||
assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "false")
|
||||
assertCount2(data.User[16], 3, "incoming", "all", "false")
|
||||
assertCount2(data.User[16], 3, "incoming", "confirmed", "false")
|
||||
assertCount2(data.User[16], 0, "incoming", "unconfirmed", "false")
|
||||
|
||||
clist := tt.RequestAuthGet[chanlist](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data.User[16].UID))
|
||||
chan1 := langext.ArrFirstOrNil(clist.Channels, func(v chanobj) bool { return v.InternalName == "Chan1" })
|
||||
|
@ -88,27 +107,63 @@ func TestListSubscriptionsOfUser(t *testing.T) {
|
|||
|
||||
tt.RequestAuthDelete[gin.H](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[16].UID, sub3["subscription_id"]), gin.H{})
|
||||
|
||||
assertCount(data.User[16], 3, "outgoing_all")
|
||||
assertCount(data.User[16], 3, "outgoing_confirmed")
|
||||
assertCount(data.User[16], 0, "outgoing_unconfirmed")
|
||||
assertCount(data.User[16], 5, "incoming_all")
|
||||
assertCount(data.User[16], 4, "incoming_confirmed")
|
||||
assertCount(data.User[16], 1, "incoming_unconfirmed")
|
||||
assertCount(data.User[16], 3, "outgoing", "all")
|
||||
assertCount(data.User[16], 3, "outgoing", "confirmed")
|
||||
assertCount(data.User[16], 0, "outgoing", "unconfirmed")
|
||||
assertCount(data.User[16], 5, "incoming", "all")
|
||||
assertCount(data.User[16], 4, "incoming", "confirmed")
|
||||
assertCount(data.User[16], 1, "incoming", "unconfirmed")
|
||||
|
||||
assertCount2(data.User[16], 0, "outgoing", "all", "true")
|
||||
assertCount2(data.User[16], 0, "outgoing", "confirmed", "true")
|
||||
assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "true")
|
||||
assertCount2(data.User[16], 2, "incoming", "all", "true")
|
||||
assertCount2(data.User[16], 1, "incoming", "confirmed", "true")
|
||||
assertCount2(data.User[16], 1, "incoming", "unconfirmed", "true")
|
||||
|
||||
assertCount2(data.User[16], 3, "outgoing", "all", "false")
|
||||
assertCount2(data.User[16], 3, "outgoing", "confirmed", "false")
|
||||
assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "false")
|
||||
assertCount2(data.User[16], 3, "incoming", "all", "false")
|
||||
assertCount2(data.User[16], 3, "incoming", "confirmed", "false")
|
||||
assertCount2(data.User[16], 0, "incoming", "unconfirmed", "false")
|
||||
|
||||
tt.RequestAuthPatch[gin.H](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[16].UID, sub1["subscription_id"]), gin.H{
|
||||
"confirmed": false,
|
||||
})
|
||||
|
||||
assertCount(data.User[16], 5, "incoming_all")
|
||||
assertCount(data.User[16], 3, "incoming_confirmed")
|
||||
assertCount(data.User[16], 2, "incoming_unconfirmed")
|
||||
assertCount(data.User[16], 5, "incoming", "all")
|
||||
assertCount(data.User[16], 3, "incoming", "confirmed")
|
||||
assertCount(data.User[16], 2, "incoming", "unconfirmed")
|
||||
|
||||
assertCount(data.User[0], 7, "outgoing_all")
|
||||
assertCount(data.User[0], 5, "outgoing_confirmed")
|
||||
assertCount(data.User[0], 2, "outgoing_unconfirmed")
|
||||
assertCount(data.User[0], 5, "incoming_all")
|
||||
assertCount(data.User[0], 5, "incoming_confirmed")
|
||||
assertCount(data.User[0], 0, "incoming_unconfirmed")
|
||||
assertCount2(data.User[16], 2, "incoming", "all", "true")
|
||||
assertCount2(data.User[16], 0, "incoming", "confirmed", "true")
|
||||
assertCount2(data.User[16], 2, "incoming", "unconfirmed", "true")
|
||||
|
||||
assertCount2(data.User[16], 3, "incoming", "all", "false")
|
||||
assertCount2(data.User[16], 3, "incoming", "confirmed", "false")
|
||||
assertCount2(data.User[16], 0, "incoming", "unconfirmed", "false")
|
||||
|
||||
assertCount(data.User[0], 7, "outgoing", "all")
|
||||
assertCount(data.User[0], 5, "outgoing", "confirmed")
|
||||
assertCount(data.User[0], 2, "outgoing", "unconfirmed")
|
||||
assertCount(data.User[0], 5, "incoming", "all")
|
||||
assertCount(data.User[0], 5, "incoming", "confirmed")
|
||||
assertCount(data.User[0], 0, "incoming", "unconfirmed")
|
||||
|
||||
assertCount2(data.User[0], 2, "outgoing", "all", "true")
|
||||
assertCount2(data.User[0], 0, "outgoing", "confirmed", "true")
|
||||
assertCount2(data.User[0], 2, "outgoing", "unconfirmed", "true")
|
||||
assertCount2(data.User[0], 0, "incoming", "all", "true")
|
||||
assertCount2(data.User[0], 0, "incoming", "confirmed", "true")
|
||||
assertCount2(data.User[0], 0, "incoming", "unconfirmed", "true")
|
||||
|
||||
assertCount2(data.User[0], 5, "outgoing", "all", "false")
|
||||
assertCount2(data.User[0], 5, "outgoing", "confirmed", "false")
|
||||
assertCount2(data.User[0], 0, "outgoing", "unconfirmed", "false")
|
||||
assertCount2(data.User[0], 5, "incoming", "all", "false")
|
||||
assertCount2(data.User[0], 5, "incoming", "confirmed", "false")
|
||||
assertCount2(data.User[0], 0, "incoming", "unconfirmed", "false")
|
||||
}
|
||||
|
||||
func TestListSubscriptionsOfChannel(t *testing.T) {
|
||||
|
@ -537,9 +592,15 @@ func TestGetSubscriptionToForeignChannel(t *testing.T) {
|
|||
Channels []chanobj `json:"channels"`
|
||||
}
|
||||
|
||||
assertCount := func(u tt.Userdat, c int, sel string) {
|
||||
slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", u.UID, sel))
|
||||
tt.AssertEqual(t, sel+".len", c, len(slist.Subscriptions))
|
||||
assertCount := func(u tt.Userdat, c int, dir string, conf string) {
|
||||
slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", u.UID, dir, conf))
|
||||
tt.AssertEqual(t, dir+"."+conf+".len", c, len(slist.Subscriptions))
|
||||
}
|
||||
|
||||
assertCount2 := func(u tt.Userdat, c int, dir string, conf string, ext string) {
|
||||
slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s&external=%s", u.UID, dir, conf, ext))
|
||||
fmt.Printf("assertCount2 := %d\n", len(slist.Subscriptions))
|
||||
//tt.AssertEqual(t, dir+"."+conf+"."+ext+".len", c, len(slist.Subscriptions))
|
||||
}
|
||||
|
||||
clist := tt.RequestAuthGet[chanlist](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data.User[16].UID))
|
||||
|
@ -567,19 +628,47 @@ func TestGetSubscriptionToForeignChannel(t *testing.T) {
|
|||
|
||||
tt.RequestAuthDelete[gin.H](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[16].UID, sub3.SubscriptionId), gin.H{})
|
||||
|
||||
assertCount(data.User[16], 3, "outgoing_all")
|
||||
assertCount(data.User[16], 3, "outgoing_confirmed")
|
||||
assertCount(data.User[16], 0, "outgoing_unconfirmed")
|
||||
assertCount(data.User[16], 5, "incoming_all")
|
||||
assertCount(data.User[16], 4, "incoming_confirmed")
|
||||
assertCount(data.User[16], 1, "incoming_unconfirmed")
|
||||
assertCount(data.User[16], 3, "outgoing", "all")
|
||||
assertCount(data.User[16], 3, "outgoing", "confirmed")
|
||||
assertCount(data.User[16], 0, "outgoing", "unconfirmed")
|
||||
assertCount(data.User[16], 5, "incoming", "all")
|
||||
assertCount(data.User[16], 4, "incoming", "confirmed")
|
||||
assertCount(data.User[16], 1, "incoming", "unconfirmed")
|
||||
|
||||
assertCount(data.User[0], 7, "outgoing_all")
|
||||
assertCount(data.User[0], 6, "outgoing_confirmed")
|
||||
assertCount(data.User[0], 1, "outgoing_unconfirmed")
|
||||
assertCount(data.User[0], 5, "incoming_all")
|
||||
assertCount(data.User[0], 5, "incoming_confirmed")
|
||||
assertCount(data.User[0], 0, "incoming_unconfirmed")
|
||||
assertCount2(data.User[16], 0, "outgoing", "all", "true")
|
||||
assertCount2(data.User[16], 0, "outgoing", "confirmed", "true")
|
||||
assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "true")
|
||||
assertCount2(data.User[16], 2, "incoming", "all", "true")
|
||||
assertCount2(data.User[16], 1, "incoming", "confirmed", "true")
|
||||
assertCount2(data.User[16], 1, "incoming", "unconfirmed", "true")
|
||||
|
||||
assertCount2(data.User[16], 3, "outgoing", "all", "false")
|
||||
assertCount2(data.User[16], 3, "outgoing", "confirmed", "false")
|
||||
assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "false")
|
||||
assertCount2(data.User[16], 3, "incoming", "all", "false")
|
||||
assertCount2(data.User[16], 3, "incoming", "confirmed", "false")
|
||||
assertCount2(data.User[16], 0, "incoming", "unconfirmed", "false")
|
||||
|
||||
assertCount(data.User[0], 7, "outgoing", "all")
|
||||
assertCount(data.User[0], 6, "outgoing", "confirmed")
|
||||
assertCount(data.User[0], 1, "outgoing", "unconfirmed")
|
||||
assertCount(data.User[0], 5, "incoming", "all")
|
||||
assertCount(data.User[0], 5, "incoming", "confirmed")
|
||||
assertCount(data.User[0], 0, "incoming", "unconfirmed")
|
||||
|
||||
assertCount2(data.User[0], 2, "outgoing", "all", "true")
|
||||
assertCount2(data.User[0], 1, "outgoing", "confirmed", "true")
|
||||
assertCount2(data.User[0], 1, "outgoing", "unconfirmed", "true")
|
||||
assertCount2(data.User[0], 0, "incoming", "all", "true")
|
||||
assertCount2(data.User[0], 0, "incoming", "confirmed", "true")
|
||||
assertCount2(data.User[0], 0, "incoming", "unconfirmed", "true")
|
||||
|
||||
assertCount2(data.User[0], 5, "outgoing", "all", "false")
|
||||
assertCount2(data.User[0], 5, "outgoing", "confirmed", "false")
|
||||
assertCount2(data.User[0], 0, "outgoing", "unconfirmed", "false")
|
||||
assertCount2(data.User[0], 5, "incoming", "all", "false")
|
||||
assertCount2(data.User[0], 5, "incoming", "confirmed", "false")
|
||||
assertCount2(data.User[0], 0, "incoming", "unconfirmed", "false")
|
||||
|
||||
gsub1 := tt.RequestAuthGet[subobj](t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[0].UID, sub1.SubscriptionId))
|
||||
tt.AssertEqual(t, "SubscriptionId", sub1.SubscriptionId, gsub1.SubscriptionId)
|
||||
|
|
|
@ -0,0 +1,447 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/push"
|
||||
tt "blackforestbytes.com/simplecloudnotifier/test/util"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUptimeKumaDown(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.title", "Monitor test went down!", pusher.Last().Message.Title)
|
||||
|
||||
type mglist struct {
|
||||
Messages []gin.H `json:"messages"`
|
||||
}
|
||||
|
||||
msgList1 := tt.RequestAuthGet[mglist](t, data.AdminKey, baseUrl, "/api/v2/messages")
|
||||
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages))
|
||||
tt.AssertStrRepEqual(t, "msg.title", "Monitor test went down!", msgList1.Messages[0]["title"])
|
||||
tt.AssertStrRepEqual(t, "msg.content", "getaddrinfo ENOTFOUND exampleasdsda.com", msgList1.Messages[0]["content"])
|
||||
}
|
||||
|
||||
func TestUptimeKumaUp(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [✅ Up] 200 - OK",
|
||||
"heartbeat": gin.H{
|
||||
"status": 1,
|
||||
"msg": "200 - OK",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.title", "Monitor test is back online", pusher.Last().Message.Title)
|
||||
|
||||
type mglist struct {
|
||||
Messages []gin.H `json:"messages"`
|
||||
}
|
||||
|
||||
msgList1 := tt.RequestAuthGet[mglist](t, data.AdminKey, baseUrl, "/api/v2/messages")
|
||||
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages))
|
||||
tt.AssertStrRepEqual(t, "msg.title", "Monitor test is back online", msgList1.Messages[0]["title"])
|
||||
tt.AssertStrRepEqual(t, "msg.content", "200 - OK", msgList1.Messages[0]["content"])
|
||||
}
|
||||
|
||||
func TestUptimeKumaFullDown(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
ts := time.Now().Add(-time.Hour).Format("2006-01-02 15:04:05")
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, tt.RawJSON{ContentType: "application/json", Body: `{"heartbeat":{"monitorID":89,"status":0,"time":"` + ts + `","msg":"timeout of 16000ms exceeded","important":true,"duration":36,"timezone":"Europe/Berlin","timezoneOffset":"+02:00","localDateTime":"` + ts + `"},"monitor":{"id":89,"name":"test","description":null,"pathName":"test","parent":null,"childrenIDs":[],"url":"https://exampleXYZ.com","method":"GET","hostname":null,"port":null,"maxretries":1,"weight":2000,"active":true,"forceInactive":false,"type":"http","interval":20,"retryInterval":20,"resendInterval":0,"keyword":null,"expiryNotification":false,"ignoreTls":false,"upsideDown":false,"packetSize":56,"maxredirects":10,"accepted_statuscodes":["200-299"],"dns_resolve_type":"A","dns_resolve_server":"1.1.1.1","dns_last_result":null,"docker_container":"","docker_host":null,"proxyId":null,"notificationIDList":{"2":true},"tags":[],"maintenance":false,"mqttTopic":"","mqttSuccessMessage":"","databaseQuery":null,"authMethod":null,"grpcUrl":null,"grpcProtobuf":null,"grpcMethod":null,"grpcServiceName":null,"grpcEnableTls":false,"radiusCalledStationId":null,"radiusCallingStationId":null,"game":null,"httpBodyEncoding":"json","includeSensitiveData":false},"msg":"[test] [🔴 Down] timeout of 16000ms exceeded"}`})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.title", "Monitor test went down!", pusher.Last().Message.Title)
|
||||
tt.AssertStrRepEqual(t, "msg.title", "timeout of 16000ms exceeded", pusher.Last().Message.Content)
|
||||
|
||||
type mglist struct {
|
||||
Messages []gin.H `json:"messages"`
|
||||
}
|
||||
|
||||
msgList1 := tt.RequestAuthGet[mglist](t, data.AdminKey, baseUrl, "/api/v2/messages")
|
||||
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages))
|
||||
tt.AssertStrRepEqual(t, "msg.title", "Monitor test went down!", msgList1.Messages[0]["title"])
|
||||
tt.AssertStrRepEqual(t, "msg.content", "timeout of 16000ms exceeded", msgList1.Messages[0]["content"])
|
||||
}
|
||||
|
||||
func TestUptimeKumaFullUp(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
ts := time.Now().Add(-time.Hour).Format("2006-01-02 15:04:05")
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, tt.RawJSON{ContentType: "application/json", Body: `{"heartbeat":{"monitorID":89,"status":1,"time":"` + ts + `","msg":"200 - OK","ping":55,"important":true,"duration":41,"timezone":"Europe/Berlin","timezoneOffset":"+02:00","localDateTime":"` + ts + `"},"monitor":{"id":89,"name":"test","description":null,"pathName":"test","parent":null,"childrenIDs":[],"url":"https://example.com","method":"GET","hostname":null,"port":null,"maxretries":1,"weight":2000,"active":true,"forceInactive":false,"type":"http","interval":20,"retryInterval":20,"resendInterval":0,"keyword":null,"expiryNotification":false,"ignoreTls":false,"upsideDown":false,"packetSize":56,"maxredirects":10,"accepted_statuscodes":["200-299"],"dns_resolve_type":"A","dns_resolve_server":"1.1.1.1","dns_last_result":null,"docker_container":"","docker_host":null,"proxyId":null,"notificationIDList":{"2":true},"tags":[],"maintenance":false,"mqttTopic":"","mqttSuccessMessage":"","databaseQuery":null,"authMethod":null,"grpcUrl":null,"grpcProtobuf":null,"grpcMethod":null,"grpcServiceName":null,"grpcEnableTls":false,"radiusCalledStationId":null,"radiusCallingStationId":null,"game":null,"httpBodyEncoding":"json","includeSensitiveData":false},"msg":"[test] [✅ Up] 200 - OK"}`})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.title", "Monitor test is back online", pusher.Last().Message.Title)
|
||||
|
||||
type mglist struct {
|
||||
Messages []gin.H `json:"messages"`
|
||||
}
|
||||
|
||||
msgList1 := tt.RequestAuthGet[mglist](t, data.AdminKey, baseUrl, "/api/v2/messages")
|
||||
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages))
|
||||
tt.AssertStrRepEqual(t, "msg.title", "Monitor test is back online", msgList1.Messages[0]["title"])
|
||||
tt.AssertStrRepEqual(t, "msg.content", "200 - OK", msgList1.Messages[0]["content"])
|
||||
}
|
||||
|
||||
func TestUptimeKumaChannelNone(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.channel", "main", pusher.Last().Message.ChannelInternalName)
|
||||
}
|
||||
|
||||
func TestUptimeKumaChannelSingle(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&channel=CTEST", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.channel", "CTEST", pusher.Last().Message.ChannelInternalName)
|
||||
}
|
||||
|
||||
func TestUptimeKumaChannelAllDown(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&channel=CTEST&channel_up=CTEST_UP&channel_down=CTEST_DOWN", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.channel", "CTEST_DOWN", pusher.Last().Message.ChannelInternalName)
|
||||
}
|
||||
|
||||
func TestUptimeKumaChannelSpecDown(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&channel_up=CTEST_UP&channel_down=CTEST_DOWN", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.channel", "CTEST_DOWN", pusher.Last().Message.ChannelInternalName)
|
||||
}
|
||||
|
||||
func TestUptimeKumaChannelAllUp(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&channel=CTEST&channel_up=CTEST_UP&channel_down=CTEST_DOWN", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [✅ Up] 200 - OK",
|
||||
"heartbeat": gin.H{
|
||||
"status": 1,
|
||||
"msg": "200 - OK",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.channel", "CTEST_UP", pusher.Last().Message.ChannelInternalName)
|
||||
}
|
||||
|
||||
func TestUptimeKumaChannelSpecUp(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&channel_up=CTEST_UP&channel_down=CTEST_DOWN", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [✅ Up] 200 - OK",
|
||||
"heartbeat": gin.H{
|
||||
"status": 1,
|
||||
"msg": "200 - OK",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.channel", "CTEST_UP", pusher.Last().Message.ChannelInternalName)
|
||||
}
|
||||
|
||||
func TestUptimeKumaPriorityNone(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.channel", 1, pusher.Last().Message.Priority)
|
||||
}
|
||||
|
||||
func TestUptimeKumaPrioritySingle(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix0 := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&priority=0", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix0, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.prio", 0, pusher.Last().Message.Priority)
|
||||
|
||||
suffix1 := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&priority=1", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix1, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 2, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.prio", 1, pusher.Last().Message.Priority)
|
||||
|
||||
suffix2 := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&priority=2", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix2, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 3, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.prio", 2, pusher.Last().Message.Priority)
|
||||
}
|
||||
|
||||
func TestUptimeKumaPriorityAllDown(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&priority=1&priority_up=2&priority_down=0", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.prio", 0, pusher.Last().Message.Priority)
|
||||
}
|
||||
|
||||
func TestUptimeKumaPrioritySpecDown(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&priority_up=2&priority_down=0", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [🔴 Down] getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
"heartbeat": gin.H{
|
||||
"status": 0,
|
||||
"msg": "getaddrinfo ENOTFOUND exampleasdsda.com",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.prio", 0, pusher.Last().Message.Priority)
|
||||
}
|
||||
|
||||
func TestUptimeKumaPriorityAllUp(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&priority=1&priority_up=2&priority_down=0", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [✅ Up] 200 - OK",
|
||||
"heartbeat": gin.H{
|
||||
"status": 1,
|
||||
"msg": "200 - OK",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.prio", 2, pusher.Last().Message.Priority)
|
||||
}
|
||||
|
||||
func TestUptimeKumaPrioritySpecUp(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
data := tt.InitSingleData(t, ws)
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
|
||||
suffix := fmt.Sprintf("/external/v1/uptime-kuma?user_id=%v&key=%v&priority_up=2&priority_down=0", data.UID, data.SendKey)
|
||||
_ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{
|
||||
"msg": "[test] [✅ Up] 200 - OK",
|
||||
"heartbeat": gin.H{
|
||||
"status": 1,
|
||||
"msg": "200 - OK",
|
||||
},
|
||||
"monitor": gin.H{
|
||||
"name": "test",
|
||||
},
|
||||
})
|
||||
|
||||
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
|
||||
tt.AssertStrRepEqual(t, "msg.prio", 2, pusher.Last().Message.Priority)
|
||||
}
|
|
@ -428,3 +428,70 @@ func TestUserMessageCounter(t *testing.T) {
|
|||
|
||||
assertCounter(5)
|
||||
}
|
||||
|
||||
func TestGetUserNoPro(t *testing.T) {
|
||||
_, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/v2/users", gin.H{
|
||||
"no_client": true,
|
||||
})
|
||||
|
||||
uid := fmt.Sprintf("%v", r0["user_id"])
|
||||
readtok := r0["read_key"].(string)
|
||||
|
||||
r1 := tt.RequestAuthGet[gin.H](t, readtok, baseUrl, "/api/v2/users/"+uid)
|
||||
|
||||
tt.AssertEqual(t, "user_id", uid, fmt.Sprintf("%v", r1["user_id"]))
|
||||
tt.AssertEqual(t, "username", nil, r1["username"])
|
||||
tt.AssertNotEqual(t, "timestamp_created", nil, r1["timestamp_created"])
|
||||
tt.AssertEqual(t, "timestamp_lastread", nil, r1["timestamp_lastread"])
|
||||
tt.AssertEqual(t, "timestamp_lastsent", nil, r1["timestamp_lastsent"])
|
||||
tt.AssertEqual(t, "messages_sent", "0", fmt.Sprintf("%v", r1["messages_sent"]))
|
||||
tt.AssertEqual(t, "quota_used", "0", fmt.Sprintf("%v", r1["quota_used"]))
|
||||
tt.AssertEqual(t, "quota_remaining", "50", fmt.Sprintf("%v", r1["quota_remaining"]))
|
||||
tt.AssertEqual(t, "quota_max", "50", fmt.Sprintf("%v", r1["quota_max"]))
|
||||
tt.AssertEqual(t, "is_pro", "false", fmt.Sprintf("%v", r1["is_pro"]))
|
||||
tt.AssertEqual(t, "default_channel", "main", fmt.Sprintf("%v", r1["default_channel"]))
|
||||
tt.AssertEqual(t, "max_body_size", "2048", fmt.Sprintf("%v", r1["max_body_size"]))
|
||||
tt.AssertEqual(t, "max_title_length", "120", fmt.Sprintf("%v", r1["max_title_length"]))
|
||||
tt.AssertEqual(t, "default_priority", "1", fmt.Sprintf("%v", r1["default_priority"]))
|
||||
tt.AssertEqual(t, "max_channel_name_length", "120", fmt.Sprintf("%v", r1["max_channel_name_length"]))
|
||||
tt.AssertEqual(t, "max_channel_description_length", "300", fmt.Sprintf("%v", r1["max_channel_description_length"]))
|
||||
tt.AssertEqual(t, "max_sender_name_length", "120", fmt.Sprintf("%v", r1["max_sender_name_length"]))
|
||||
tt.AssertEqual(t, "max_user_message_id_length", "64", fmt.Sprintf("%v", r1["max_user_message_id_length"]))
|
||||
}
|
||||
|
||||
func TestGetUserPro(t *testing.T) {
|
||||
_, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/v2/users", gin.H{
|
||||
"no_client": true,
|
||||
"pro_token": "ANDROID|v2|PURCHASED:DUMMY_TOK_XX",
|
||||
})
|
||||
|
||||
uid := fmt.Sprintf("%v", r0["user_id"])
|
||||
readtok := r0["read_key"].(string)
|
||||
|
||||
r1 := tt.RequestAuthGet[gin.H](t, readtok, baseUrl, "/api/v2/users/"+uid)
|
||||
|
||||
tt.AssertEqual(t, "user_id", uid, fmt.Sprintf("%v", r1["user_id"]))
|
||||
tt.AssertEqual(t, "username", nil, r1["username"])
|
||||
tt.AssertNotEqual(t, "timestamp_created", nil, r1["timestamp_created"])
|
||||
tt.AssertEqual(t, "timestamp_lastread", nil, r1["timestamp_lastread"])
|
||||
tt.AssertEqual(t, "timestamp_lastsent", nil, r1["timestamp_lastsent"])
|
||||
tt.AssertEqual(t, "messages_sent", "0", fmt.Sprintf("%v", r1["messages_sent"]))
|
||||
tt.AssertEqual(t, "quota_used", "0", fmt.Sprintf("%v", r1["quota_used"]))
|
||||
tt.AssertEqual(t, "quota_remaining", "5000", fmt.Sprintf("%v", r1["quota_remaining"]))
|
||||
tt.AssertEqual(t, "quota_max", "5000", fmt.Sprintf("%v", r1["quota_max"]))
|
||||
tt.AssertEqual(t, "is_pro", "true", fmt.Sprintf("%v", r1["is_pro"]))
|
||||
tt.AssertEqual(t, "default_channel", "main", fmt.Sprintf("%v", r1["default_channel"]))
|
||||
tt.AssertEqual(t, "max_body_size", "2097152", fmt.Sprintf("%d", (int64)(r1["max_body_size"].(float64))))
|
||||
tt.AssertEqual(t, "max_title_length", "120", fmt.Sprintf("%v", r1["max_title_length"]))
|
||||
tt.AssertEqual(t, "default_priority", "1", fmt.Sprintf("%v", r1["default_priority"]))
|
||||
tt.AssertEqual(t, "max_channel_name_length", "120", fmt.Sprintf("%v", r1["max_channel_name_length"]))
|
||||
tt.AssertEqual(t, "max_channel_description_length", "300", fmt.Sprintf("%v", r1["max_channel_description_length"]))
|
||||
tt.AssertEqual(t, "max_sender_name_length", "120", fmt.Sprintf("%v", r1["max_sender_name_length"]))
|
||||
tt.AssertEqual(t, "max_user_message_id_length", "64", fmt.Sprintf("%v", r1["max_user_message_id_length"]))
|
||||
}
|
||||
|
|
|
@ -302,6 +302,10 @@ func AssertArrAny[T any](t *testing.T, key string, arr []T, fn func(T) bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func AssertAny(v any) {
|
||||
// used to prevent golang "unused variable error"
|
||||
}
|
||||
|
||||
func unpointer(v any) any {
|
||||
if v == nil {
|
||||
return v
|
||||
|
|
|
@ -510,7 +510,7 @@ func doUnsubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat
|
|||
Subscriptions []gin.H `json:"subscriptions"`
|
||||
}
|
||||
|
||||
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=outgoing_confirmed", user.UID))
|
||||
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=outgoing&confirmation=confirmed", user.UID))
|
||||
|
||||
var subdat gin.H
|
||||
for _, v := range slist.Subscriptions {
|
||||
|
@ -530,7 +530,7 @@ func doAcceptSub(t *testing.T, baseUrl string, user Userdat, subscriber Userdat,
|
|||
Subscriptions []gin.H `json:"subscriptions"`
|
||||
}
|
||||
|
||||
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=incoming_unconfirmed", user.UID))
|
||||
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=incoming&confirmation=unconfirmed", user.UID))
|
||||
|
||||
var subdat gin.H
|
||||
for _, v := range slist.Subscriptions {
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
package util
|
||||
|
||||
type FormData map[string]string
|
||||
|
||||
type RawJSON struct {
|
||||
ContentType string
|
||||
Body string
|
||||
}
|
||||
|
|
|
@ -114,6 +114,9 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s
|
|||
}
|
||||
bytesbody = bodybuffer.Bytes()
|
||||
contentType = writer.FormDataContentType()
|
||||
case RawJSON:
|
||||
bytesbody = []byte(body.(RawJSON).Body)
|
||||
contentType = "application/json"
|
||||
default:
|
||||
bjson, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
|
@ -150,7 +153,11 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s
|
|||
|
||||
TPrintln("")
|
||||
TPrintf("---------------- RESPONSE (%d) ----------------\n", resp.StatusCode)
|
||||
TPrintln(langext.TryPrettyPrintJson(string(respBodyBin)))
|
||||
if len(respBodyBin) > 100_000 {
|
||||
TPrintln("[[RESPONSE TOO LONG]]")
|
||||
} else {
|
||||
TPrintln(langext.TryPrettyPrintJson(string(respBodyBin)))
|
||||
}
|
||||
TryPrintTraceObj("---------------- -------- ----------------", respBodyBin, "")
|
||||
TPrintln("---------------- -------- ----------------")
|
||||
TPrintln("")
|
||||
|
|
|
@ -75,32 +75,9 @@ func StartSimpleWebserver(t *testing.T) (*logic.Application, string, func()) {
|
|||
TPrintln("DatabaseFile<requests>: " + dbfile2)
|
||||
TPrintln("DatabaseFile<logs>: " + dbfile3)
|
||||
|
||||
conf, ok := scn.GetConfig("local-host")
|
||||
if !ok {
|
||||
TestFail(t, "conf not found")
|
||||
}
|
||||
scn.Conf = CreateTestConfig(t, dbfile1, dbfile2, dbfile3)
|
||||
|
||||
conf.ServerPort = "0" // simply choose a free port
|
||||
conf.DBMain.File = dbfile1
|
||||
conf.DBLogs.File = dbfile2
|
||||
conf.DBRequests.File = dbfile3
|
||||
conf.DBMain.Timeout = 500 * time.Millisecond
|
||||
conf.DBLogs.Timeout = 500 * time.Millisecond
|
||||
conf.DBRequests.Timeout = 500 * time.Millisecond
|
||||
conf.DBMain.ConnMaxLifetime = 1 * time.Second
|
||||
conf.DBLogs.ConnMaxLifetime = 1 * time.Second
|
||||
conf.DBRequests.ConnMaxLifetime = 1 * time.Second
|
||||
conf.DBMain.ConnMaxIdleTime = 1 * time.Second
|
||||
conf.DBLogs.ConnMaxIdleTime = 1 * time.Second
|
||||
conf.DBRequests.ConnMaxIdleTime = 1 * time.Second
|
||||
conf.RequestMaxRetry = 32
|
||||
conf.RequestRetrySleep = 100 * time.Millisecond
|
||||
conf.ReturnRawErrors = true
|
||||
conf.DummyFirebase = true
|
||||
|
||||
scn.Conf = conf
|
||||
|
||||
sqlite, err := logic.NewDBPool(conf)
|
||||
sqlite, err := logic.NewDBPool(scn.Conf)
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
|
@ -111,7 +88,7 @@ func StartSimpleWebserver(t *testing.T) (*logic.Application, string, func()) {
|
|||
TestFailErr(t, err)
|
||||
}
|
||||
|
||||
ginengine := ginext.NewEngine(conf)
|
||||
ginengine := ginext.NewEngine(scn.Conf)
|
||||
|
||||
router := api.NewRouter(app)
|
||||
|
||||
|
@ -119,7 +96,7 @@ func StartSimpleWebserver(t *testing.T) (*logic.Application, string, func()) {
|
|||
|
||||
apc := google.NewDummy()
|
||||
|
||||
app.Init(conf, ginengine, nc, apc, []logic.Job{
|
||||
app.Init(scn.Conf, ginengine, nc, apc, []logic.Job{
|
||||
jobs.NewDeliveryRetryJob(app),
|
||||
jobs.NewRequestLogCollectorJob(app),
|
||||
})
|
||||
|
@ -148,3 +125,99 @@ func StartSimpleWebserver(t *testing.T) (*logic.Application, string, func()) {
|
|||
|
||||
return app, "http://127.0.0.1:" + app.Port, stop
|
||||
}
|
||||
|
||||
func StartSimpleTestspace(t *testing.T) (string, string, string, scn.Config, func()) {
|
||||
InitTests()
|
||||
|
||||
uuid1, _ := langext.NewHexUUID()
|
||||
uuid2, _ := langext.NewHexUUID()
|
||||
uuid3, _ := langext.NewHexUUID()
|
||||
|
||||
dbdir := t.TempDir()
|
||||
dbfile1 := filepath.Join(dbdir, uuid1+".sqlite3")
|
||||
dbfile2 := filepath.Join(dbdir, uuid2+".sqlite3")
|
||||
dbfile3 := filepath.Join(dbdir, uuid3+".sqlite3")
|
||||
|
||||
err := os.MkdirAll(dbdir, os.ModePerm)
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
|
||||
f1, err := os.Create(dbfile1)
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
err = f1.Close()
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
err = os.Chmod(dbfile1, 0777)
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
f2, err := os.Create(dbfile2)
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
err = f2.Close()
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
err = os.Chmod(dbfile2, 0777)
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
f3, err := os.Create(dbfile3)
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
err = f3.Close()
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
err = os.Chmod(dbfile3, 0777)
|
||||
if err != nil {
|
||||
TestFailErr(t, err)
|
||||
}
|
||||
|
||||
TPrintln("DatabaseFile<main>: " + dbfile1)
|
||||
TPrintln("DatabaseFile<requests>: " + dbfile2)
|
||||
TPrintln("DatabaseFile<logs>: " + dbfile3)
|
||||
|
||||
scn.Conf = CreateTestConfig(t, dbfile1, dbfile2, dbfile3)
|
||||
|
||||
stop := func() {
|
||||
_ = os.Remove(dbfile1)
|
||||
_ = os.Remove(dbfile2)
|
||||
_ = os.Remove(dbfile3)
|
||||
}
|
||||
|
||||
return dbfile1, dbfile2, dbfile3, scn.Conf, stop
|
||||
}
|
||||
|
||||
func CreateTestConfig(t *testing.T, dbfile1 string, dbfile2 string, dbfile3 string) scn.Config {
|
||||
conf, ok := scn.GetConfig("local-host")
|
||||
if !ok {
|
||||
TestFail(t, "conf not found")
|
||||
}
|
||||
|
||||
conf.ServerPort = "0" // simply choose a free port
|
||||
conf.DBMain.File = dbfile1
|
||||
conf.DBLogs.File = dbfile2
|
||||
conf.DBRequests.File = dbfile3
|
||||
conf.DBMain.Timeout = 500 * time.Millisecond
|
||||
conf.DBLogs.Timeout = 500 * time.Millisecond
|
||||
conf.DBRequests.Timeout = 500 * time.Millisecond
|
||||
conf.DBMain.ConnMaxLifetime = 1 * time.Second
|
||||
conf.DBLogs.ConnMaxLifetime = 1 * time.Second
|
||||
conf.DBRequests.ConnMaxLifetime = 1 * time.Second
|
||||
conf.DBMain.ConnMaxIdleTime = 1 * time.Second
|
||||
conf.DBLogs.ConnMaxIdleTime = 1 * time.Second
|
||||
conf.DBRequests.ConnMaxIdleTime = 1 * time.Second
|
||||
conf.RequestMaxRetry = 32
|
||||
conf.RequestRetrySleep = 100 * time.Millisecond
|
||||
conf.ReturnRawErrors = true
|
||||
conf.DummyFirebase = true
|
||||
|
||||
return conf
|
||||
}
|
||||
|
|
|
@ -14,13 +14,10 @@
|
|||
<span class="c1"># or scn_send "@${channel} "${title}" ${content}"</span>
|
||||
<span class="c1"># or scn_send "@${channel} "${title}" ${content}" "${priority:0|1|2}"</span>
|
||||
<span class="c1">#</span>
|
||||
<span class="c1"># content can be of format "--scnsend-read-body-from-file={path}" to read body from file</span>
|
||||
<span class="c1"># (this circumvents max commandline length)</span>
|
||||
<span class="c1">#</span>
|
||||
|
||||
<span class="c1">################################################################################</span>
|
||||
<span class="c1"># INSERT YOUR DATA HERE #</span>
|
||||
<span class="c1">################################################################################</span>
|
||||
<span class="nv">user_id</span><span class="o">=</span><span class="s2">"999"</span><span class="w"> </span><span class="c1"># your user_id</span>
|
||||
<span class="nv">user_key</span><span class="o">=</span><span class="s2">"??"</span><span class="w"> </span><span class="c1"># use userkey with SEND permissions on the used channel</span>
|
||||
<span class="c1">################################################################################</span>
|
||||
|
||||
usage<span class="o">()</span><span class="w"> </span><span class="o">{</span>
|
||||
|
@ -34,16 +31,40 @@ usage<span class="o">()</span><span class="w"> </span><span class="o">{</span>
|
|||
<span class="k">function</span><span class="w"> </span>rederr<span class="o">()</span><span class="w"> </span><span class="o">{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span>cfgcol<span class="p">;</span><span class="w"> </span><span class="k">then</span><span class="w"> </span>><span class="p">&</span><span class="m">2</span><span class="w"> </span><span class="nb">echo</span><span class="w"> </span>-e<span class="w"> </span><span class="s2">"\x1B[31m</span><span class="nv">$1</span><span class="s2">\x1B[0m"</span><span class="p">;</span><span class="w"> </span><span class="k">else</span><span class="w"> </span>><span class="p">&</span><span class="m">2</span><span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span><span class="p">;</span><span class="w"> </span><span class="k">fi</span><span class="p">;</span><span class="w"> </span><span class="o">}</span>
|
||||
<span class="k">function</span><span class="w"> </span>green<span class="o">()</span><span class="w"> </span><span class="o">{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span>cfgcol<span class="p">;</span><span class="w"> </span><span class="k">then</span><span class="w"> </span><span class="nb">echo</span><span class="w"> </span>-e<span class="w"> </span><span class="s2">"\x1B[32m</span><span class="nv">$1</span><span class="s2">\x1B[0m"</span><span class="p">;</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span><span class="p">;</span><span class="w"> </span><span class="k">fi</span><span class="p">;</span><span class="w"> </span><span class="o">}</span>
|
||||
|
||||
<span class="c1">################################################################################</span>
|
||||
|
||||
<span class="c1">#</span>
|
||||
<span class="c1"># Get env 'SCN_UID' and 'SCN_KEY' from conf file</span>
|
||||
<span class="c1"># </span>
|
||||
<span class="c1"># shellcheck source=/dev/null</span>
|
||||
.<span class="w"> </span><span class="s2">"/etc/scn.conf"</span>
|
||||
<span class="nv">SCN_UID</span><span class="o">=</span><span class="si">${</span><span class="nv">SCN_UID</span><span class="k">:-</span><span class="si">}</span>
|
||||
<span class="nv">SCN_KEY</span><span class="o">=</span><span class="si">${</span><span class="nv">SCN_KEY</span><span class="k">:-</span><span class="si">}</span>
|
||||
|
||||
<span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">"</span><span class="si">${</span><span class="nv">SCN_UID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="o">{</span><span class="w"> </span>rederr<span class="w"> </span><span class="s2">"Missing config value 'SCN_UID' in /etc/scn.conf"</span><span class="p">;</span><span class="w"> </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span><span class="p">;</span><span class="w"> </span><span class="o">}</span>
|
||||
<span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">"</span><span class="si">${</span><span class="nv">SCN_KEY</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="o">{</span><span class="w"> </span>rederr<span class="w"> </span><span class="s2">"Missing config value 'SCN_KEY' in /etc/scn.conf"</span><span class="p">;</span><span class="w"> </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span><span class="p">;</span><span class="w"> </span><span class="o">}</span>
|
||||
|
||||
<span class="c1">################################################################################</span>
|
||||
|
||||
<span class="nv">args</span><span class="o">=(</span><span class="w"> </span><span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
|
||||
<span class="nv">title</span><span class="o">=</span><span class="s2">""</span>
|
||||
<span class="nv">content</span><span class="o">=</span><span class="s2">""</span>
|
||||
<span class="nv">channel</span><span class="o">=</span><span class="s2">""</span>
|
||||
<span class="nv">priority</span><span class="o">=</span><span class="m">1</span>
|
||||
<span class="nv">priority</span><span class="o">=</span><span class="s2">""</span>
|
||||
<span class="nv">usr_msg_id</span><span class="o">=</span><span class="s2">"</span><span class="k">$(</span>head<span class="w"> </span>/dev/urandom<span class="w"> </span><span class="p">|</span><span class="w"> </span>tr<span class="w"> </span>-dc<span class="w"> </span>A-Za-z0-9<span class="w"> </span><span class="p">|</span><span class="w"> </span>head<span class="w"> </span>-c<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">"</span>
|
||||
<span class="nv">sendtime</span><span class="o">=</span><span class="s2">"</span><span class="k">$(</span>date<span class="w"> </span>+%s<span class="k">)</span><span class="s2">"</span>
|
||||
<span class="nv">sender</span><span class="o">=</span><span class="s2">"</span><span class="k">$(</span>hostname<span class="k">)</span><span class="s2">"</span>
|
||||
|
||||
<span class="k">if</span><span class="w"> </span><span class="nb">command</span><span class="w"> </span>-v<span class="w"> </span>srvname<span class="w"> </span><span class="p">&</span>><span class="w"> </span>/dev/null<span class="p">;</span><span class="w"> </span><span class="k">then</span>
|
||||
<span class="w"> </span><span class="nv">sender</span><span class="o">=</span><span class="s2">"</span><span class="k">$(</span><span class="w"> </span>srvname<span class="w"> </span><span class="k">)</span><span class="s2">"</span>
|
||||
<span class="k">fi</span>
|
||||
|
||||
<span class="k">if</span><span class="w"> </span><span class="o">[[</span><span class="w"> </span><span class="s2">"</span><span class="si">${</span><span class="nv">args</span><span class="p">[0]</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"--"</span><span class="w"> </span><span class="o">]]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
|
||||
<span class="w"> </span><span class="c1"># only positional args form here on (currently not handled)</span>
|
||||
<span class="w"> </span><span class="nv">args</span><span class="o">=(</span><span class="s2">"</span><span class="si">${</span><span class="nv">args</span><span class="p">[@]:</span><span class="nv">1</span><span class="si">}</span><span class="s2">"</span><span class="o">)</span>
|
||||
<span class="k">fi</span>
|
||||
|
||||
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span><span class="si">${#</span><span class="nv">args</span><span class="p">[@]</span><span class="si">}</span><span class="w"> </span>-lt<span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
|
||||
<span class="w"> </span>rederr<span class="w"> </span><span class="s2">"[ERROR]: no title supplied via parameter"</span>
|
||||
<span class="w"> </span>usage
|
||||
|
@ -83,28 +104,33 @@ usage<span class="o">()</span><span class="w"> </span><span class="o">{</span>
|
|||
<span class="w"> </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span>
|
||||
<span class="k">fi</span>
|
||||
|
||||
<span class="k">if</span><span class="w"> </span><span class="o">[[</span><span class="w"> </span><span class="s2">"</span><span class="nv">$content</span><span class="s2">"</span><span class="w"> </span><span class="o">==</span><span class="w"> </span>--scnsend-read-body-from-file<span class="o">=</span>*<span class="w"> </span><span class="o">]]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
|
||||
<span class="w"> </span><span class="nv">path</span><span class="o">=</span><span class="s2">"</span><span class="k">$(</span><span class="w"> </span>awk<span class="w"> </span><span class="s1">'{ print substr($0, 31) }'</span><span class="w"> </span><span class="o"><<<</span><span class="w"> </span><span class="s2">"</span><span class="nv">$content</span><span class="s2">"</span><span class="w"> </span><span class="k">)</span><span class="s2">"</span>
|
||||
<span class="w"> </span><span class="nv">content</span><span class="o">=</span><span class="s2">"</span><span class="k">$(</span><span class="w"> </span>cat<span class="w"> </span><span class="s2">"</span><span class="nv">$path</span><span class="s2">"</span><span class="w"> </span><span class="k">)</span><span class="s2">"</span>
|
||||
<span class="k">fi</span>
|
||||
|
||||
<span class="nv">curlparams</span><span class="o">=()</span>
|
||||
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data"</span><span class="w"> </span><span class="s2">"user_id=</span><span class="si">${</span><span class="nv">SCN_UID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data"</span><span class="w"> </span><span class="s2">"key=</span><span class="si">${</span><span class="nv">SCN_KEY</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data"</span><span class="w"> </span><span class="s2">"title=</span><span class="nv">$title</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data"</span><span class="w"> </span><span class="s2">"timestamp=</span><span class="nv">$sendtime</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data"</span><span class="w"> </span><span class="s2">"msg_id=</span><span class="nv">$usr_msg_id</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data-urlencode"</span><span class="w"> </span><span class="s2">"user_id=</span><span class="si">${</span><span class="nv">SCN_UID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data-urlencode"</span><span class="w"> </span><span class="s2">"key=</span><span class="si">${</span><span class="nv">SCN_KEY</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data-urlencode"</span><span class="w"> </span><span class="s2">"title=</span><span class="nv">$title</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data-urlencode"</span><span class="w"> </span><span class="s2">"timestamp=</span><span class="nv">$sendtime</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
<span class="nv">curlparams</span><span class="o">+=(</span><span class="w"> </span><span class="s2">"--data-urlencode"</span><span class="w"> </span><span class="s2">"msg_id=</span><span class="nv">$usr_msg_id</span><span class="s2">"</span><span class="w"> </span><span class="o">)</span>
|
||||
|
||||
<span class="k">if</span><span class="w"> </span><span class="o">[[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">"</span><span class="nv">$content</span><span class="s2">"</span><span class="w"> </span><span class="o">]]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
|
||||
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">"--data"</span><span class="w"> </span><span class="s2">"content=</span><span class="nv">$content</span><span class="s2">"</span><span class="o">)</span>
|
||||
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">"--data-urlencode"</span><span class="w"> </span><span class="s2">"content=</span><span class="nv">$content</span><span class="s2">"</span><span class="o">)</span>
|
||||
<span class="k">fi</span>
|
||||
|
||||
<span class="k">if</span><span class="w"> </span><span class="o">[[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">"</span><span class="nv">$priority</span><span class="s2">"</span><span class="w"> </span><span class="o">]]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
|
||||
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">"--data"</span><span class="w"> </span><span class="s2">"priority=</span><span class="nv">$priority</span><span class="s2">"</span><span class="o">)</span>
|
||||
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">"--data-urlencode"</span><span class="w"> </span><span class="s2">"priority=</span><span class="nv">$priority</span><span class="s2">"</span><span class="o">)</span>
|
||||
<span class="k">fi</span>
|
||||
|
||||
<span class="k">if</span><span class="w"> </span><span class="o">[[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">"</span><span class="nv">$channel</span><span class="s2">"</span><span class="w"> </span><span class="o">]]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
|
||||
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">"--data"</span><span class="w"> </span><span class="s2">"channel=</span><span class="nv">$channel</span><span class="s2">"</span><span class="o">)</span>
|
||||
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">"--data-urlencode"</span><span class="w"> </span><span class="s2">"channel=</span><span class="nv">$channel</span><span class="s2">"</span><span class="o">)</span>
|
||||
<span class="k">fi</span>
|
||||
|
||||
<span class="k">if</span><span class="w"> </span><span class="o">[[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">"</span><span class="nv">$sender</span><span class="s2">"</span><span class="w"> </span><span class="o">]]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
|
||||
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">"--data"</span><span class="w"> </span><span class="s2">"sender_name=</span><span class="nv">$sender</span><span class="s2">"</span><span class="o">)</span>
|
||||
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">"--data-urlencode"</span><span class="w"> </span><span class="s2">"sender_name=</span><span class="nv">$sender</span><span class="s2">"</span><span class="o">)</span>
|
||||
<span class="k">fi</span>
|
||||
|
||||
<span class="k">while</span><span class="w"> </span><span class="nb">true</span><span class="w"> </span><span class="p">;</span><span class="w"> </span><span class="k">do</span>
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
|
||||
# Created by https://www.gitignore.io/api/git,windows,intellij,phpstorm+all
|
||||
|
||||
### Git ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
# $ git config --global mergetool.keepBackup false
|
||||
*.orig
|
||||
|
||||
# Created by git when using merge tools for conflicts
|
||||
*.BACKUP.*
|
||||
*.BASE.*
|
||||
*.LOCAL.*
|
||||
*.REMOTE.*
|
||||
*_BACKUP_*.txt
|
||||
*_BASE_*.txt
|
||||
*_LOCAL_*.txt
|
||||
*_REMOTE_*.txt
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### PhpStorm+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
|
||||
# Generated files
|
||||
|
||||
# Sensitive or high-churn files
|
||||
|
||||
# Gradle
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
|
||||
# CMake
|
||||
|
||||
# Mongo Explorer plugin
|
||||
|
||||
# File-based project format
|
||||
|
||||
# IntelliJ
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
|
||||
# JIRA plugin
|
||||
|
||||
# Cursive Clojure plugin
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
|
||||
# Editor-based Rest Client
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
|
||||
### PhpStorm+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
|
||||
# End of https://www.gitignore.io/api/git,windows,intellij,phpstorm+all
|
||||
|
||||
|
||||
|
||||
#################
|
||||
|
||||
config.php
|
||||
.verify_accesstoken
|
57
web/api.php
57
web/api.php
|
@ -1,57 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php
|
||||
if (file_exists('/var/www/openwebanalytics/owa_php.php'))
|
||||
{
|
||||
require_once('/var/www/openwebanalytics/owa_php.php');
|
||||
$owa = new owa_php();
|
||||
$owa->setSiteId('6386b0efc00d2e84ef642525345e1207');
|
||||
$owa->setPageTitle('API (Short)');
|
||||
$owa->trackPageView();
|
||||
}
|
||||
?>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
|
||||
<title>Simple Cloud Notifications - API</title>
|
||||
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||
</div>
|
||||
|
||||
<div id="mainpnl">
|
||||
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
|
||||
|
||||
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||
|
||||
<p>Get your user-id and user-key from the app and send notifications to your phone by performing a POST request against <code>https://simplecloudnotifier.blackforestbytes.com/send.php</code></p>
|
||||
<pre>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}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
<p>The <code>content</code>, <code>priority</code> and <code>msg_id</code> parameters are optional, you can also send message with only a title and the default priority</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
|
||||
<a href="/api_more.php" class="button bordered tertiary" style="float: right; min-width: 100px; text-align: center">More</a>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,2 +0,0 @@
|
|||
config.php
|
||||
.verify_accesstoken
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
// insert your values here and rename to config.php
|
||||
|
||||
return
|
||||
[
|
||||
'global' =>
|
||||
[
|
||||
'prod' => true,
|
||||
],
|
||||
|
||||
'database' =>
|
||||
[
|
||||
'host' => '?',
|
||||
'database' => '?',
|
||||
'user' => '?',
|
||||
'password' => '?',
|
||||
],
|
||||
|
||||
'firebase' =>
|
||||
[
|
||||
'type' => 'service_account',
|
||||
'project_id' => '?',
|
||||
'private_key_id' => '???',
|
||||
'client_email' => '???.iam.gserviceaccount.com',
|
||||
'client_id' => '???',
|
||||
'auth_uri' => 'https://accounts.google.com/o/oauth2/auth',
|
||||
'token_uri' => 'https://oauth2.googleapis.com/token',
|
||||
'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
'client_x509_cert_url' => 'https://www.googleapis.com/robot/v1/metadata/x509/???f.iam.gserviceaccount.com',
|
||||
'private_key' => "-----BEGIN PRIVATE KEY-----\n"
|
||||
. "??????????\n"
|
||||
. "-----END PRIVATE KEY-----\n",
|
||||
'server_key' => '????',
|
||||
],
|
||||
|
||||
'verify_api' =>
|
||||
[
|
||||
'package_name' => 'com.blackforestbytes.simplecloudnotifier',
|
||||
'product_id' => '???',
|
||||
|
||||
'clientid' => '???.apps.googleusercontent.com',
|
||||
'clientsecret' => '???',
|
||||
'accesstoken' => file_exists('.verify_accesstoken') ? file_get_contents('.verify_accesstoken') : '',
|
||||
'refreshtoken' => '???',
|
||||
'scope' => 'https://www.googleapis.com/auth/androidpublisher',
|
||||
],
|
||||
|
||||
'error_reporting' =>
|
||||
[
|
||||
'send-mail' => true,
|
||||
'email-error-target' => '???@???.com',
|
||||
'email-error-sender' => '???@???.com',
|
||||
],
|
||||
|
||||
];
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errid'=>101, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errid'=>102, 'message' => 'Missing parameter [[user_key]]']));
|
||||
if (!isset($INPUT['scn_msg_id'])) die(json_encode(['success' => false, 'errid'=>103, 'message' => 'Missing parameter [[scn_msg_id]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
$scn_msg_id = $INPUT['scn_msg_id'];
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>201, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
|
||||
|
||||
$stmt = $pdo->prepare('SELECT ack FROM messages WHERE scn_message_id=:smid AND sender_user_id=:uid LIMIT 1');
|
||||
$stmt->execute(['smid' => $scn_msg_id, 'uid' => $user_id]);
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>301, 'message' => 'Message not found']));
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE messages SET ack=1 WHERE scn_message_id=:smid AND sender_user_id=:uid');
|
||||
$stmt->execute(['smid' => $scn_msg_id, 'uid' => $user_id]);
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'prev_ack' => $datas[0]['ack'],
|
||||
'new_ack' => 1,
|
||||
'message' => 'ok'
|
||||
]);
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errid'=>101, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errid'=>102, 'message' => 'Missing parameter [[user_key]]']));
|
||||
if (!isset($INPUT['scn_msg_id'])) die(json_encode(['success' => false, 'errid'=>103, 'message' => 'Missing parameter [[scn_msg_id]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
$scn_msg_id = $INPUT['scn_msg_id'];
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>201, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM messages WHERE scn_message_id=:smid AND sender_user_id=:uid LIMIT 1');
|
||||
$stmt->execute(['smid' => $scn_msg_id, 'uid' => $user_id]);
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>301, 'message' => 'Message not found']));
|
||||
|
||||
$msg = $datas[0];
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'data' =>
|
||||
[
|
||||
'title' => $msg['title'],
|
||||
'body' => $msg['content'],
|
||||
'trimmed' => false,
|
||||
'priority' => $msg['priority'],
|
||||
'timestamp' => $msg['sendtime'],
|
||||
'usr_msg_id' => $msg['usr_message_id'],
|
||||
'scn_msg_id' => $msg['scn_message_id'],
|
||||
],
|
||||
'message' => 'ok'
|
||||
]);
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errid'=>101, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errid'=>102, 'message' => 'Missing parameter [[user_key]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>201, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
|
||||
|
||||
|
||||
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM messages WHERE ack=0 AND sender_user_id=:uid');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
$nack_count = $stmt->fetch(PDO::FETCH_NUM)[0];
|
||||
|
||||
|
||||
$quota = $data['quota_today'];
|
||||
$is_pro = $data['is_pro'];
|
||||
|
||||
if ($data['quota_day'] === null || $data['quota_day'] !== date("Y-m-d")) $quota=0;
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'message' => 'ok',
|
||||
'user_id' => $user_id,
|
||||
'quota' => $quota,
|
||||
'quota_max' => Statics::quota_max($is_pro),
|
||||
'is_pro' => $is_pro,
|
||||
'fcm_token_set' => ($data['fcm_token'] != null),
|
||||
'unack_count' => $nack_count,
|
||||
]);
|
Binary file not shown.
|
@ -1,276 +0,0 @@
|
|||
<?php
|
||||
|
||||
include('lib/httpful.phar');
|
||||
|
||||
class ERR
|
||||
{
|
||||
const NO_ERROR = 0000;
|
||||
|
||||
const MISSING_UID = 1101;
|
||||
const MISSING_TOK = 1102;
|
||||
const MISSING_TITLE = 1103;
|
||||
const INVALID_PRIO = 1104;
|
||||
const REQ_METHOD = 1105;
|
||||
|
||||
const NO_TITLE = 1201;
|
||||
const TITLE_TOO_LONG = 1202;
|
||||
const CONTENT_TOO_LONG = 1203;
|
||||
const USR_MSG_ID_TOO_LONG = 1204;
|
||||
const TIMESTAMP_OUT_OF_RANGE = 1205;
|
||||
|
||||
const USER_NOT_FOUND = 1301;
|
||||
const USER_AUTH_FAILED = 1302;
|
||||
|
||||
const NO_DEVICE_LINKED = 1401;
|
||||
|
||||
const QUOTA_REACHED = 2101;
|
||||
|
||||
const FIREBASE_COM_FAILED = 9901;
|
||||
const FIREBASE_COM_ERRORED = 9902;
|
||||
const INTERNAL_EXCEPTION = 9903;
|
||||
}
|
||||
|
||||
class Statics
|
||||
{
|
||||
public static $DB = NULL;
|
||||
public static $CFG = NULL;
|
||||
|
||||
public static function quota_max($is_pro) { return $is_pro ? 1000 : 50; }
|
||||
|
||||
public static function contentlen_max($is_pro) { return $is_pro ? 16384 : 2048; }
|
||||
}
|
||||
|
||||
function str_limit($str, $len)
|
||||
{
|
||||
if (strlen($str)>$len) return substr($str, 0, $len-3)."...";
|
||||
return $str;
|
||||
}
|
||||
|
||||
function getConfig()
|
||||
{
|
||||
if (Statics::$CFG !== NULL) return Statics::$CFG;
|
||||
|
||||
return Statics::$CFG = require "config.php";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String $msg
|
||||
* @param Exception $e
|
||||
*/
|
||||
function reportError($msg, $e = null)
|
||||
{
|
||||
if ($e != null) $msg = ($msg."\n\n[[EXCEPTION]]\n" . $e . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString());
|
||||
|
||||
$subject = "SCN_Server has encountered an Error at " . date("Y-m-d H:i:s") . "] ";
|
||||
|
||||
$content = "";
|
||||
|
||||
$content .= 'HTTP_HOST: ' . ParamServerOrUndef('HTTP_HOST') . "\n";
|
||||
$content .= 'REQUEST_URI: ' . ParamServerOrUndef('REQUEST_URI') . "\n";
|
||||
$content .= 'TIME: ' . date('Y-m-d H:i:s') . "\n";
|
||||
$content .= 'REMOTE_ADDR: ' . ParamServerOrUndef('REMOTE_ADDR') . "\n";
|
||||
$content .= 'HTTP_X_FORWARDED_FOR: ' . ParamServerOrUndef('HTTP_X_FORWARDED_FOR') . "\n";
|
||||
$content .= 'HTTP_USER_AGENT: ' . ParamServerOrUndef('HTTP_USER_AGENT') . "\n";
|
||||
$content .= 'MESSAGE:' . "\n" . $msg . "\n";
|
||||
$content .= '$_GET:' . "\n" . print_r($_GET, true) . "\n";
|
||||
$content .= '$_POST:' . "\n" . print_r($_POST, true) . "\n";
|
||||
$content .= '$_FILES:' . "\n" . print_r($_FILES, true) . "\n";
|
||||
|
||||
if (getConfig()['error_reporting']['send-mail']) sendMail($subject, $content, getConfig()['error_reporting']['email-error-target'], getConfig()['error_reporting']['email-error-sender']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $subject
|
||||
* @param string $content
|
||||
* @param string $to
|
||||
* @param string $from
|
||||
*/
|
||||
function sendMail($subject, $content, $to, $from) {
|
||||
mail($to, $subject, $content, 'From: ' . $from);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $idx
|
||||
* @return string
|
||||
*/
|
||||
function ParamServerOrUndef($idx) {
|
||||
return isset($_SERVER[$idx]) ? $_SERVER[$idx] : 'NOT_SET';
|
||||
}
|
||||
|
||||
function getDatabase()
|
||||
{
|
||||
if (Statics::$DB !== NULL) return Statics::$DB;
|
||||
|
||||
$_config = getConfig()['database'];
|
||||
|
||||
$dsn = "mysql:host=" . $_config['host'] . ";dbname=" . $_config['database'] . ";charset=utf8";
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
return Statics::$DB = new PDO($dsn, $_config['user'], $_config['password'], $opt);
|
||||
}
|
||||
|
||||
function generateRandomAuthKey()
|
||||
{
|
||||
$random = '';
|
||||
for ($i = 0; $i < 64; $i++)
|
||||
|
||||
try {
|
||||
switch (random_int(1, 3)) {
|
||||
case 1:
|
||||
$random .= chr(random_int(ord('0'), ord('9')));
|
||||
break;
|
||||
case 2:
|
||||
$random .= chr(random_int(ord('A'), ord('Z')));
|
||||
break;
|
||||
case 3:
|
||||
$random .= chr(random_int(ord('a'), ord('z')));
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
die(json_encode(['success' => false, 'message' => 'Internal error - no randomness']));
|
||||
}
|
||||
return $random;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $url
|
||||
* @param $body
|
||||
* @param $header
|
||||
* @return array|object|string
|
||||
* @throws \Httpful\Exception\ConnectionErrorException
|
||||
* @throws Exception
|
||||
*/
|
||||
function sendPOST($url, $body, $header)
|
||||
{
|
||||
$builder = \Httpful\Request::post($url);
|
||||
|
||||
$builder->body($body);
|
||||
|
||||
foreach ($header as $k => $v) $builder->addHeader($k, $v);
|
||||
|
||||
$response = $builder->send();
|
||||
|
||||
if ($response->code != 200) throw new Exception("Repsponse code: " . $response->code);
|
||||
|
||||
return $response->raw_body;
|
||||
}
|
||||
|
||||
function verifyOrderToken($tok)
|
||||
{
|
||||
// https://developers.google.com/android-publisher/api-ref/purchases/products/get
|
||||
|
||||
// if this does no longer work, you probably have to go through the initial OAuth process again
|
||||
// 1. go to Postman do the [ https://accounts.google.com/o/oauth2/auth ] request (in browser) to get a new "code"
|
||||
// 2. go to Postman do the [ Get Tokens ] request to get a new "access_token" and "access_token"
|
||||
// 3. update these tokens in the server config.php
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
$package = getConfig()['verify_api']['package_name'];
|
||||
$product = getConfig()['verify_api']['product_id'];
|
||||
$acctoken = getConfig()['verify_api']['accesstoken'];
|
||||
|
||||
if ($acctoken == '' || $acctoken == null || $acctoken == false) $acctoken = refreshVerifyToken();
|
||||
|
||||
$url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$package.'/purchases/products/'.$product.'/tokens/'.$tok.'?access_token='.$acctoken;
|
||||
$response = $builder = \Httpful\Request::get($url)->send();
|
||||
$obj = json_decode($response->raw_body, true);
|
||||
|
||||
if ($response->code != 401 && ($obj === null || $obj === false))
|
||||
{
|
||||
reportError('verify-token returned NULL');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($response->code == 401 || isset($obj['error']) && isset($obj['error']['code']) && $obj['error']['code'] == 401) // "Invalid Credentials" -- refresh acces_token
|
||||
{
|
||||
$acctoken = refreshVerifyToken();
|
||||
|
||||
$url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$package.'/purchases/products/'.$product.'/tokens/'.$tok.'?access_token='.$acctoken;
|
||||
$response = $builder = \Httpful\Request::get($url)->send();
|
||||
$obj = json_decode($response->raw_body, true);
|
||||
|
||||
if ($obj === null || $obj === false)
|
||||
{
|
||||
reportError('verify-token returned NULL');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($obj['purchaseState']) && $obj['purchaseState'] === 0) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
reportError("VerifyOrder token threw exception", $e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
function refreshVerifyToken()
|
||||
{
|
||||
$url = 'https://accounts.google.com/o/oauth2/token'.
|
||||
'?grant_type=refresh_token'.
|
||||
'&refresh_token='.getConfig()['verify_api']['refreshtoken'].
|
||||
'&client_id='.getConfig()['verify_api']['clientid'].
|
||||
'&client_secret='.getConfig()['verify_api']['clientsecret'];
|
||||
|
||||
$json = sendPOST($url, "", []);
|
||||
$obj = json_decode($json, true);
|
||||
file_put_contents('.verify_accesstoken', $obj['access_token']);
|
||||
|
||||
return $obj['access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $http_code
|
||||
* @param array $message
|
||||
*/
|
||||
function api_return($http_code, $message)
|
||||
{
|
||||
http_response_code($http_code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($message);
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String $str
|
||||
* @param String[] $path
|
||||
* @return mixed|null
|
||||
*/
|
||||
function try_json($str, $path)
|
||||
{
|
||||
try
|
||||
{
|
||||
$o = json_decode($str, true);
|
||||
foreach ($path as $p) $o = $o[$p];
|
||||
return $o;
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//#################################################################################################################
|
||||
|
||||
if (getConfig()['global']['prod']) {
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
} else {
|
||||
error_reporting(E_STRICT);
|
||||
ini_set('display_errors', 1);
|
||||
}
|
||||
|
||||
//#################################################################################################################
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
if (!isset($INPUT['fcm_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[fcm_token]]']));
|
||||
if (!isset($INPUT['pro'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro]]']));
|
||||
if (!isset($INPUT['pro_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro_token]]']));
|
||||
|
||||
$fcmtoken = $INPUT['fcm_token'];
|
||||
$ispro = $INPUT['pro'] == 'true';
|
||||
$pro_token = $INPUT['pro_token'];
|
||||
$user_key = generateRandomAuthKey();
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($ispro)
|
||||
{
|
||||
if (!verifyOrderToken($pro_token))
|
||||
{
|
||||
$pdo->rollBack();
|
||||
die(json_encode(['success' => false, 'message' => 'Purchase token could not be verified']));
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO users (user_key, fcm_token, is_pro, pro_token, timestamp_accessed) VALUES (:key, :token, :bpro, :spro, NOW())');
|
||||
$stmt->execute(['key' => $user_key, 'token' => $fcmtoken, 'bpro' => ($ispro ? 1 : 0), 'spro' => ($ispro ? $pro_token : null)]);
|
||||
$user_id = $pdo->lastInsertId('user_id');
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET fcm_token=NULL WHERE user_id <> :uid AND fcm_token=:ft');
|
||||
$stmt->execute(['uid' => $user_id, 'ft' => $fcmtoken]);
|
||||
|
||||
if ($ispro)
|
||||
{
|
||||
$stmt = $pdo->prepare('UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> :uid AND pro_token = :ptk');
|
||||
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'user_key' => $user_key,
|
||||
'quota' => 0,
|
||||
'quota_max' => Statics::quota_max($ispro),
|
||||
'is_pro' => $ispro,
|
||||
'message' => 'New user registered'
|
||||
]);
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errid'=>101, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errid'=>102, 'message' => 'Missing parameter [[user_key]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>201, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
|
||||
|
||||
//-------------------
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM messages WHERE ack=0 AND sender_user_id=:uid ORDER BY `timestamp_real` DESC LIMIT 16');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
$nonacks_sql = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$nonacks = [];
|
||||
|
||||
foreach ($nonacks_sql as $nack)
|
||||
{
|
||||
$nonacks []=
|
||||
[
|
||||
'title' => $nack['title'],
|
||||
'body' => $nack['content'],
|
||||
'priority' => $nack['priority'],
|
||||
'timestamp' => $nack['sendtime'],
|
||||
'usr_msg_id' => $nack['usr_message_id'],
|
||||
'scn_msg_id' => $nack['scn_message_id'],
|
||||
];
|
||||
}
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'message' => 'ok',
|
||||
'count' => count($nonacks),
|
||||
'data' => $nonacks,
|
||||
]);
|
|
@ -1,38 +0,0 @@
|
|||
DROP TABLE IF EXISTS `users`;
|
||||
CREATE TABLE `users`
|
||||
(
|
||||
`user_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_key` VARCHAR(64) NOT NULL,
|
||||
`fcm_token` VARCHAR(256) NULL DEFAULT NULL,
|
||||
`messages_sent` INT(11) NOT NULL DEFAULT '0',
|
||||
`timestamp_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`timestamp_accessed` DATETIME NULL DEFAULT NULL,
|
||||
|
||||
`quota_today` INT(11) NOT NULL DEFAULT '0',
|
||||
`quota_day` DATE NULL DEFAULT NULL,
|
||||
|
||||
`is_pro` BIT NOT NULL DEFAULT 0,
|
||||
`pro_token` VARCHAR(256) NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (`user_id`)
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS `messages`;
|
||||
CREATE TABLE `messages`
|
||||
(
|
||||
`scn_message_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`sender_user_id` INT(11) NOT NULL,
|
||||
|
||||
`timestamp_real` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`ack` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
|
||||
`title` VARCHAR(256) NOT NULL,
|
||||
`content` LONGTEXT NULL,
|
||||
`priority` INT(11) NOT NULL,
|
||||
`sendtime` BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
`fcm_message_id` VARCHAR(256) NULL,
|
||||
`usr_message_id` VARCHAR(256) NULL,
|
||||
|
||||
PRIMARY KEY (`scn_message_id`)
|
||||
);
|
|
@ -1,73 +0,0 @@
|
|||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_key]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
$fcm_token = isset($INPUT['fcm_token']) ? $INPUT['fcm_token'] : null;
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_day, is_pro FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'message' => 'Authentification failed']));
|
||||
|
||||
$quota = $data['quota_today'];
|
||||
$is_pro = $data['is_pro'];
|
||||
|
||||
$new_userkey = generateRandomAuthKey();
|
||||
|
||||
if ($fcm_token === null)
|
||||
{
|
||||
// only gen new user_secret
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), user_key=:at WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $user_id, 'at' => $new_userkey]);
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'user_key' => $new_userkey,
|
||||
'quota' => $quota,
|
||||
'quota_max'=> Statics::quota_max($data['is_pro']),
|
||||
'is_pro' => $is_pro,
|
||||
'message' => 'user updated'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// update fcm and gen new user_secret
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), fcm_token=:ft, user_key=:at WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $user_id, 'ft' => $fcm_token, 'at' => $new_userkey]);
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET fcm_token=NULL WHERE user_id <> :uid AND fcm_token=:ft');
|
||||
$stmt->execute(['uid' => $user_id, 'ft' => $fcm_token]);
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'user_key' => $new_userkey,
|
||||
'quota' => $quota,
|
||||
'quota_max'=> Statics::quota_max($data['is_pro']),
|
||||
'is_pro' => $is_pro,
|
||||
'message' => 'user updated'
|
||||
]);
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_key]]']));
|
||||
if (!isset($INPUT['pro'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro]]']));
|
||||
if (!isset($INPUT['pro_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro_token]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
$ispro = $INPUT['pro'] == 'true';
|
||||
$pro_token = $INPUT['pro_token'];
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_day, is_pro, pro_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'message' => 'Authentification failed']));
|
||||
|
||||
if ($ispro)
|
||||
{
|
||||
// set pro=true
|
||||
|
||||
if ($data['pro_token'] != $pro_token)
|
||||
{
|
||||
if (!verifyOrderToken($pro_token)) die(json_encode(['success' => false, 'message' => 'Purchase token could not be verified']));
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), is_pro=1, pro_token=:ptk WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> :uid AND pro_token = :ptk');
|
||||
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'quota' => $data['quota_today'],
|
||||
'quota_max'=> Statics::quota_max(true),
|
||||
'is_pro' => true,
|
||||
'message' => 'user updated'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set pro=false
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), is_pro=0, pro_token=NULL WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'quota' => $data['quota_today'],
|
||||
'quota_max'=> Statics::quota_max(false),
|
||||
'is_pro' => false,
|
||||
'message' => 'user updated'
|
||||
]);
|
||||
}
|
309
web/api_more.php
309
web/api_more.php
|
@ -1,309 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php
|
||||
if (file_exists('/var/www/openwebanalytics/owa_php.php'))
|
||||
{
|
||||
require_once('/var/www/openwebanalytics/owa_php.php');
|
||||
$owa = new owa_php();
|
||||
$owa->setSiteId('6386b0efc00d2e84ef642525345e1207');
|
||||
$owa->setPageTitle('API (Long)');
|
||||
$owa->trackPageView();
|
||||
}
|
||||
?>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
|
||||
<title>Simple Cloud Notifications - API</title>
|
||||
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||
</div>
|
||||
|
||||
<div id="mainpnl">
|
||||
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
|
||||
|
||||
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||
|
||||
<h2>Introduction</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
With this API you can send push notifications to your phone.
|
||||
</p>
|
||||
<p>
|
||||
To recieve them you will need to install the <a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier">SimpleCloudNotifier</a> app from the play store.
|
||||
When you open the app you can click on the account tab to see you unique <code>user_id</code> and <code>user_key</code>.
|
||||
These two values are used to identify and authenticate your device so that send messages can be routed to your phone.
|
||||
</p>
|
||||
<p>
|
||||
You can at any time generate a new <code>user_key</code> in the app and invalidate the old one.
|
||||
</p>
|
||||
<p>
|
||||
There is also a <a href="/index.php">web interface</a> for this API to manually send notifications to your phone or to test your setup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>Quota</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
By default you can send up to 100 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).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>API Requests</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
To send a new notification you send a <code>POST</code> request to the URL <code>https://scn.blackforestbytes.com/send.php</code>.
|
||||
All Parameters can either directly be submitted as URL parameters or they can be put into the POST body.
|
||||
</p>
|
||||
<p>
|
||||
You <i>need</i> to supply a valid <code>user_id</code> - <code>user_key</code> pair and a <code>title</code> for your message, all other parameter are optional.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>API Response</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
If the operation was successful the API will respond with an HTTP statuscode 200 and an JSON payload indicating the send message and your remaining quota
|
||||
</p>
|
||||
<pre class="red-code">{
|
||||
"success":true,
|
||||
"message":"Message sent",
|
||||
"response":
|
||||
{
|
||||
"multicast_id":8000000000000000006,
|
||||
"success":1,
|
||||
"failure":0,
|
||||
"canonical_ids":0,
|
||||
"results": [{"message_id":"0:10000000000000000000000000000000d"}]
|
||||
},
|
||||
"quota":17,
|
||||
"quota_max":100
|
||||
}</pre>
|
||||
<p>
|
||||
If the operation is <b>not</b> successful the API will respond with an 4xx HTTP statuscode.
|
||||
</p>
|
||||
<table class="scode_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Statuscode</th>
|
||||
<th>Explanation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td data-label="Statuscode">200 (OK)</td>
|
||||
<td data-label="Explanation">Message sent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">400 (Bad Request)</td>
|
||||
<td data-label="Explanation">The request is invalid (missing parameters or wrong values)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">401 (Unauthorized)</td>
|
||||
<td data-label="Explanation">The user_id was not found or the user_key is wrong</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">403 (Forbidden)</td>
|
||||
<td data-label="Explanation">The user has exceeded its daily quota - wait 24 hours or upgrade your account</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">412 (Precondition Failed)</td>
|
||||
<td data-label="Explanation">There is no device connected with this account - open the app and press the refresh button in the account tab</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">500 (Internal Server Error)</td>
|
||||
<td data-label="Explanation">There was an internal error while sending your data - try again later</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
There is also always a JSON payload with additional information.
|
||||
The <code>success</code> field is always there and in the error state you the <code>message</code> field to get a descritpion of the problem.
|
||||
</p>
|
||||
<pre class="red-code">{
|
||||
"success":false,
|
||||
"error":2101,
|
||||
"errhighlight":-1,
|
||||
"message":"Daily quota reached (100)"
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<h2>Message Content</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
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.
|
||||
You can see the whole message with title and content in the app or when clicking on the notification.
|
||||
</p>
|
||||
<p>
|
||||
If needed the content can be supplied in the <code>content</code> parameter.
|
||||
</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
--data "content={message_content}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
</div>
|
||||
|
||||
<h2>Message Priority</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
Currently you can send a message with three different priorities: 0 (low), 1 (normal) and 2 (high).
|
||||
In the app you can then configure a different behaviour for different priorities, e.g. only playing a sound if the notification is high priority.
|
||||
</p>
|
||||
<p>
|
||||
Priorites are either 0, 1 or 2 and are supplied in the <code>priority</code> parameter.
|
||||
If no priority is supplied the message will get the default priority of 1.
|
||||
</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
--data "priority={0|1|2}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
</div>
|
||||
|
||||
<h2>Message Uniqueness</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
Sometimes your script can run in an environment with an unstable connection and you want to implement an automatic re-try mechanism to send a message again if the last try failed due to bad connectivity.
|
||||
</p>
|
||||
<p>
|
||||
To ensure that a message is only send once you can generate a unique id for your message (I would recommend a simple <code>uuidgen</code>).
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
The message_id is optional - but if you want to use it you need to supply it via the <code>msg_id</code> parameter.
|
||||
</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
--data "msg_id={message_id}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>Custom Time</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
You can modify the displayed timestamp of a message by sending the <code>timestamp</code> parameter. The format must be a valid UNIX timestamp (elapsed seconds since 1970-01-01 GMT)
|
||||
</p>
|
||||
<p>
|
||||
The custom timestamp must be within 48 hours of the current time. This parameter is only intended to supply a more precise value in case the message sending was delayed.
|
||||
</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
--data "timestamp={unix_timestamp}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
</div>
|
||||
|
||||
<h2>Bash script example</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
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.<br/>
|
||||
Here is an example how such a scrippt could look like, you can put it into <code>/usr/local/sbin</code> and call it with <code>scn_send "title" "content"</code>
|
||||
</p>
|
||||
<pre style="color:#000000;" class="yellow-code"><span style="color:#3f7f59; font-weight:bold;">#!/usr/bin/env bash</span>
|
||||
|
||||
<span style="color:#3f7f59; ">#</span>
|
||||
<span style="color:#3f7f59; "># Call with `scn_send title`</span>
|
||||
<span style="color:#3f7f59; "># or `scn_send title content`</span>
|
||||
<span style="color:#3f7f59; "># or `scn_send title content priority`</span>
|
||||
<span style="color:#3f7f59; ">#</span>
|
||||
<span style="color:#3f7f59; ">#</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -lt 1 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"no title supplied via parameter"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#3f7f59; ">################################################################################</span>
|
||||
<span style="color:#3f7f59; "># INSERT YOUR DATA HERE #</span>
|
||||
<span style="color:#3f7f59; ">################################################################################</span>
|
||||
user_id=999
|
||||
user_key=<span style="color:#2a00ff; ">"????????????????????????????????????????????????????????????????"</span>
|
||||
<span style="color:#3f7f59; ">################################################################################</span>
|
||||
|
||||
title=$1
|
||||
content=<span style="color:#2a00ff; ">""</span>
|
||||
sendtime=$(date +%s)
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -gt 1 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
content=$2
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
priority=1
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -gt 2 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
priority=$3
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
usr_msg_id=$(uuidgen)
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">while</span> true ; <span style="color:#7f0055; font-weight:bold; ">do</span>
|
||||
|
||||
curlresp=$(curl -s -o <span style="color:#3f3fbf; ">/dev/null</span> -w <span style="color:#2a00ff; ">"%{http_code}"</span> <span style="color:#2a00ff; ">\</span>
|
||||
-d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">user_id</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$user_id</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">user_key</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$user_key</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">title</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$title</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">timestamp</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$sendtime</span><span style="color:#2a00ff; ">"</span> <span style="color:#2a00ff; ">\</span>
|
||||
-d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">content</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$content</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">priority</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$priority</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">msg_id</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$usr_msg_id</span><span style="color:#2a00ff; ">"</span> <span style="color:#2a00ff; ">\</span>
|
||||
https:<span style="color:#3f3fbf; ">/</span><span style="color:#3f3fbf; ">/scn.blackforestbytes.com/send.php</span>)
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 200 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Successfully send"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 0
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 400 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Bad request - something went wrong"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 401 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Unauthorized - wrong </span><span style="color:#3f3fbf; ">userid/userkey</span><span style="color:#2a00ff; ">"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 403 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Quota exceeded - wait one hour before re-try"</span>
|
||||
sleep 3600
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 412 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Precondition Failed - No device linked"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 500 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Internal server error - waiting for better times"</span>
|
||||
sleep 60
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#3f7f59; "># if none of the above matched we probably hav no network ...</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Send failed (response code </span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">) ... try again in 5s"</span>
|
||||
sleep 5
|
||||
<span style="color:#7f0055; font-weight:bold; ">done</span>
|
||||
</pre>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,250 +0,0 @@
|
|||
|
||||
html
|
||||
{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
|
||||
}
|
||||
|
||||
@keyframes blink-shadow {
|
||||
0% { box-shadow: 0 0 32px #DDD; }
|
||||
50% { box-shadow: none; }
|
||||
100% { box-shadow: 0 0 32px #DDD; }
|
||||
}
|
||||
|
||||
#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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
.red-code
|
||||
{
|
||||
border-left: .25rem solid #E53935;
|
||||
}
|
||||
|
||||
.yellow-code
|
||||
{
|
||||
border-left: .25rem solid #FFCB05;
|
||||
}
|
||||
|
||||
#mainpnl input,
|
||||
#mainpnl textarea
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.responsive-label {
|
||||
align-items:center;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.responsive-label .col-md-3 {
|
||||
text-align:right
|
||||
}
|
||||
}
|
||||
|
||||
#mainpnl h1
|
||||
{
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 24px;
|
||||
font-weight: bold;
|
||||
color: #FFF;
|
||||
text-shadow: #000 0 0 2px, #888 0 0 8px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
#mainpnl h1 {
|
||||
font-size: calc(0.85rem * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio));
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
#mainpnl button
|
||||
{
|
||||
width: 100%;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#copyinfo
|
||||
{
|
||||
margin: 4px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
//z-index: -999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#copyinfo a,
|
||||
#copyinfo a:visited,
|
||||
#copyinfo a:active
|
||||
{
|
||||
font-family: "Courier New", monospace;
|
||||
color: #AAA;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
#copyinfo a:hover
|
||||
{
|
||||
font-family: "Courier New", monospace;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
#tr_link
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: -1px -1px 0 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#tl_link
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: -1px 0 0 -1px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
padding: 4px 4px 0 4px;
|
||||
}
|
||||
|
||||
.icn-google-play {
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IgogICAgIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIKICAgICB2aWV3Qm94PSIwIDAgNDggNDgiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDsiPjxnIGlkPSJzdXJmYWNlMSI+PHBhdGggc3R5bGU9IiBmaWxsOiM0REI2QUM7IiBkPSJNIDcuNzAzMTI1IDQuMDQyOTY5IEMgNy4yOTI5NjkgNC4xNDg0MzggNyA0LjUwNzgxMyA3IDUuMTIxMDk0IEMgNyA2LjkyMTg3NSA3IDIzLjkxNDA2MyA3IDIzLjkxNDA2MyBDIDcgMjMuOTE0MDYzIDcgNDIuMjgxMjUgNyA0My4wODk4NDQgQyA3IDQzLjUzNTE1NiA3LjE5NTMxMyA0My44MzU5MzggNy41IDQzLjk0NTMxMyBMIDI3LjY3OTY4OCAyMy44ODI4MTMgWiAiPjwvcGF0aD48cGF0aCBzdHlsZT0iIGZpbGw6I0RDRTc3NTsiIGQ9Ik0gMzMuMjM4MjgxIDE4LjM1OTM3NSBMIDI0LjkyOTY4OCAxMy41NjI1IEMgMjQuOTI5Njg4IDEzLjU2MjUgOS42ODM1OTQgNC43NjE3MTkgOC43ODkwNjMgNC4yNDIxODggQyA4LjQwMjM0NCA0LjAxOTUzMSA4LjAxOTUzMSAzLjk2MDkzOCA3LjcwMzEyNSA0LjA0Mjk2OSBMIDI3LjY4MzU5NCAyMy44ODI4MTMgWiAiPjwvcGF0aD48cGF0aCBzdHlsZT0iIGZpbGw6I0QzMkYyRjsiIGQ9Ik0gOC40MTc5NjkgNDMuODAwNzgxIEMgOC45NDkyMTkgNDMuNDkyMTg4IDIzLjY5OTIxOSAzNC45NzY1NjMgMzMuMjgxMjUgMjkuNDQ1MzEzIEwgMjcuNjc5Njg4IDIzLjg4MjgxMyBMIDcuNSA0My45NDUzMTMgQyA3Ljc0NjA5NCA0NC4wMzkwNjMgOC4wNjY0MDYgNDQuMDAzOTA2IDguNDE3OTY5IDQzLjgwMDc4MSBaICI+PC9wYXRoPjxwYXRoIHN0eWxlPSIgZmlsbDojRkJDMDJEOyIgZD0iTSA0MS4zOTg0MzggMjMuMDcwMzEzIEMgNDAuNjAxNTYzIDIyLjY0MDYyNSAzMy4yOTY4NzUgMTguMzk0NTMxIDMzLjI5Njg3NSAxOC4zOTQ1MzEgTCAzMy4yMzgyODEgMTguMzU5Mzc1IEwgMjcuNjc5Njg4IDIzLjg4MjgxMyBMIDMzLjI4MTI1IDI5LjQ0NTMxMyBDIDM3LjcxNDg0NCAyNi44ODY3MTkgNDEuMDQyOTY5IDI0Ljk2NDg0NCA0MS4zMzk4NDQgMjQuNzkyOTY5IEMgNDIuMjg1MTU2IDI0LjI0NjA5NCA0Mi4xOTUzMTMgMjMuNSA0MS4zOTg0MzggMjMuMDcwMzEzIFogIj48L3BhdGg+PC9nPjwvc3ZnPg==') 50% 50% no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
#btnSend
|
||||
{
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
#btnSend .spinnerbox .spinner
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
#btnSend .spinnerbox
|
||||
{
|
||||
margin: -8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance:textfield;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.input-invalid,
|
||||
.input-invalid:hover,
|
||||
.input-invalid:active
|
||||
{
|
||||
border-color: var(--input-invalid-color) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.card.success {
|
||||
--card-back-color: rgb(48, 135, 50);
|
||||
--card-border-color: rgba(0, 0, 0, 0.3);;
|
||||
}
|
||||
|
||||
.fullcenterflex
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fullcenterflex .card
|
||||
{
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
a.card,
|
||||
a.card:active,
|
||||
a.card:visited,
|
||||
a.card:hover
|
||||
{
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.card:hover
|
||||
{
|
||||
box-shadow: 0 0 16px #AAA;
|
||||
}
|
||||
|
||||
table.scode_table {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
table.scode_table td:nth-child(2) {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
table.scode_table th:nth-child(2) {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
#mainpnl h2 {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.linkcaption:hover,
|
||||
.linkcaption:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre, pre span
|
||||
{
|
||||
font-family: Menlo, Consolas, monospace;
|
||||
background: #F9F9F9;;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* Minified by jsDelivr using clean-css v4.2.0.
|
||||
* Original file: /npm/toastify.js@1.3.0/src/toastify.css
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
/*!
|
||||
* Toastify js 1.2.2
|
||||
* https://github.com/apvarun/toastify-js
|
||||
* @license MIT licensed
|
||||
*
|
||||
* Copyright (C) 2018 Varun A P
|
||||
*/
|
||||
.toastify{padding:12px 20px;color:#fff;display:inline-block;box-shadow:0 3px 6px -1px rgba(0,0,0,.12),0 10px 36px -4px rgba(77,96,232,.3);background:-webkit-linear-gradient(315deg,#73a5ff,#5477f5);background:linear-gradient(135deg,#73a5ff,#5477f5);position:fixed;opacity:0;transition:all .4s cubic-bezier(.215,.61,.355,1);border-radius:2px;cursor:pointer;text-decoration:none;max-width:calc(50% - 20px)}.toastify.on{opacity:1}.toast-close{opacity:.4;padding:0 5px}.right{right:15px}.left{left:15px}.top{top:-150px}.bottom{bottom:-150px}.rounded{border-radius:25px}.avatar{width:1.5em;height:1.5em;margin:0 5px;border-radius:2px}@media only screen and (max-width:360px){.left,.right{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}}
|
||||
/*# sourceMappingURL=/sm/734ed69e2fe87a4469526acc0a10708fa8e0211c7d4359f9e034ceb89bb5d540.map */
|
BIN
web/favicon.ico
BIN
web/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 97 KiB |
BIN
web/favicon.png
BIN
web/favicon.png
Binary file not shown.
Before Width: | Height: | Size: 90 KiB |
|
@ -1,79 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php
|
||||
if (file_exists('/var/www/openwebanalytics/owa_php.php'))
|
||||
{
|
||||
require_once('/var/www/openwebanalytics/owa_php.php');
|
||||
$owa = new owa_php();
|
||||
$owa->setSiteId('6386b0efc00d2e84ef642525345e1207');
|
||||
$owa->setPageTitle('Index');
|
||||
$owa->trackPageView();
|
||||
}
|
||||
?>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Simple Cloud Notifications</title>
|
||||
<link rel="stylesheet" href="/css/toastify.min.css"/>
|
||||
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/ -->
|
||||
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||
</div>
|
||||
|
||||
<form id="mainpnl">
|
||||
|
||||
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a tabindex="-1" href="/api.php" class="button bordered" id="tr_link">API</a>
|
||||
|
||||
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||
|
||||
<div class="row responsive-label">
|
||||
<div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div>
|
||||
<div class="col-sm-12 col-md"><input placeholder="UserID" id="uid" class="doc" <?php echo (isset($_GET['preset_user_id']) ? (' value="'.$_GET['preset_user_id'].'" '):(''));?> type="number"></div>
|
||||
</div>
|
||||
|
||||
<div class="row responsive-label">
|
||||
<div class="col-sm-12 col-md-3"><label for="ukey" class="doc">Authentification Key</label></div>
|
||||
<div class="col-sm-12 col-md"><input placeholder="Key" id="ukey" class="doc" <?php echo (isset($_GET['preset_user_key']) ? (' value="'.$_GET['preset_user_key'].'" '):(''));?> type="text" maxlength="64"></div>
|
||||
</div>
|
||||
|
||||
<div class="row responsive-label">
|
||||
<div class="col-sm-12 col-md-3"><label for="prio" class="doc">Priority</label></div>
|
||||
<div class="col-sm-12 col-md">
|
||||
<select id="prio" class="doc" type="text" style="width:100%;">
|
||||
<option value="0" <?php echo (( isset($_GET['preset_priority'])&&$_GET['preset_priority']==='0') ? 'selected':'');?>>Low</option>
|
||||
<option value="1" <?php echo ((!isset($_GET['preset_priority'])||$_GET['preset_priority']==='1') ? 'selected':'');?>>Normal</option>
|
||||
<option value="2" <?php echo (( isset($_GET['preset_priority'])&&$_GET['preset_priority']==='2') ? 'selected':'');?>>High</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row responsive-label">
|
||||
<div class="col-sm-12 col-md-3"><label for="msg" class="doc">Message Title</label></div>
|
||||
<div class="col-sm-12 col-md"><input placeholder="Message" id="msg" class="doc" <?php echo (isset($_GET['preset_title']) ? (' value="'.$_GET['preset_title'].'" '):(''));?> type="text" maxlength="80"></div>
|
||||
</div>
|
||||
|
||||
<div class="row responsive-label">
|
||||
<div class="col-sm-12 col-md-3"><label for="txt" class="doc">Message Content</label></div>
|
||||
<div class="col-sm-12 col-md"><textarea id="txt" class="doc" <?php echo (isset($_GET['preset_content']) ? (' value="'.$_GET['preset_content'].'" '):(''));?> rows="8" maxlength="2048"></textarea></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-3"></div>
|
||||
<div class="col-sm-12 col-md"><button type="submit" class="primary bordered" id="btnSend">Send</button></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="/js/logic.js" type="text/javascript" ></script>
|
||||
<script src="/js/toastify.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,90 +0,0 @@
|
|||
|
||||
function send()
|
||||
{
|
||||
let me = document.getElementById("btnSend");
|
||||
if (me.classList.contains("btn-disabled")) return;
|
||||
|
||||
me.innerHTML = "<div class=\"spinnerbox\"><div class=\"spinner primary\"></div></div>";
|
||||
|
||||
me.classList.add("btn-disabled");
|
||||
|
||||
let uid = document.getElementById("uid");
|
||||
let key = document.getElementById("ukey");
|
||||
let msg = document.getElementById("msg");
|
||||
let txt = document.getElementById("txt");
|
||||
let pio = document.getElementById("prio");
|
||||
|
||||
uid.classList.remove('input-invalid');
|
||||
key.classList.remove('input-invalid');
|
||||
msg.classList.remove('input-invalid');
|
||||
txt.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);
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/send.php', true);
|
||||
xhr.onreadystatechange = function ()
|
||||
{
|
||||
if (xhr.readyState !== 4) return;
|
||||
|
||||
console.log('Status: ' + xhr.status);
|
||||
if (xhr.status === 200 || xhr.status === 401 || xhr.status === 403 || xhr.status === 412)
|
||||
{
|
||||
let resp = JSON.parse(xhr.responseText);
|
||||
if (!resp.success || xhr.status !== 200)
|
||||
{
|
||||
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 === 105) pio.classList.add('input-invalid');
|
||||
|
||||
Toastify({
|
||||
text: resp.message,
|
||||
gravity: "top",
|
||||
positionLeft: false,
|
||||
backgroundColor: "#D32F2F",
|
||||
}).showToast();
|
||||
}
|
||||
else
|
||||
{
|
||||
window.location.href =
|
||||
'/message_sent.php' +
|
||||
'?ok=' + 1 +
|
||||
'&message_count=' + resp.messagecount +
|
||||
'"a=' + resp.quota +
|
||||
'"a_remain=' + (resp.quota_max-resp.quota) +
|
||||
'"a_max=' + resp.quota_max +
|
||||
'&preset_user_id=' + uid.value +
|
||||
'&preset_user_key=' + key.value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Toastify({
|
||||
text: 'Request failed: Statuscode=' + xhr.status,
|
||||
gravity: "top",
|
||||
positionLeft: false,
|
||||
backgroundColor: "#D32F2F",
|
||||
}).showToast();
|
||||
}
|
||||
|
||||
me.classList.remove("btn-disabled");
|
||||
me.innerHTML = "Send";
|
||||
};
|
||||
xhr.send(data);
|
||||
}
|
||||
|
||||
window.addEventListener("load",function ()
|
||||
{
|
||||
let btnSend = document.getElementById("btnSend");
|
||||
|
||||
if (btnSend !== undefined) btnSend.onclick = function () { send(); return false; };
|
||||
|
||||
},false);
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* Minified by jsDelivr using UglifyJS v3.4.3.
|
||||
* Original file: /npm/toastify-js@1.3.0/src/toastify.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
!function(t,o){"object"==typeof module&&module.exports?(require("./toastify.css"),module.exports=o()):t.Toastify=o()}(this,function(t){var i=function(t){return new i.lib.init(t)};function r(t,o){return!(!t||"string"!=typeof o)&&!!(t.className&&-1<t.className.trim().split(/\s+/gi).indexOf(o))}return i.lib=i.prototype={toastify:"1.2.2",constructor:i,init:function(t){return t||(t={}),this.options={},this.options.text=t.text||"Hi there!",this.options.duration=t.duration||3e3,this.options.selector=t.selector,this.options.callback=t.callback||function(){},this.options.destination=t.destination,this.options.newWindow=t.newWindow||!1,this.options.close=t.close||!1,this.options.gravity="bottom"==t.gravity?"bottom":"top",this.options.positionLeft=t.positionLeft||!1,this.options.backgroundColor=t.backgroundColor,this.options.avatar=t.avatar||"",this.options.className=t.className||"",this},buildToast:function(){if(!this.options)throw"Toastify is not initialized";var t=document.createElement("div");if(t.className="toastify on "+this.options.className,!0===this.options.positionLeft?t.className+=" left":t.className+=" right",t.className+=" "+this.options.gravity,this.options.backgroundColor&&(t.style.background=this.options.backgroundColor),t.innerHTML=this.options.text,""!==this.options.avatar){var o=document.createElement("img");o.src=this.options.avatar,o.className="avatar",!0===this.options.positionLeft?t.appendChild(o):t.insertAdjacentElement("beforeend",o)}if(!0===this.options.close){var i=document.createElement("span");i.innerHTML="✖",i.className="toast-close",i.addEventListener("click",function(t){t.stopPropagation(),this.removeElement(t.target.parentElement),window.clearTimeout(t.target.parentElement.timeOutValue)}.bind(this));var n=0<window.innerWidth?window.innerWidth:screen.width;!0===this.options.positionLeft&&360<n?t.insertAdjacentElement("afterbegin",i):t.appendChild(i)}return void 0!==this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),!0===this.options.newWindow?window.open(this.options.destination,"_blank"):window.location=this.options.destination}.bind(this)),t},showToast:function(){var t,o=this.buildToast();if(!(t=void 0===this.options.selector?document.body:document.getElementById(this.options.selector)))throw"Root element is not defined";return t.insertBefore(o,t.firstChild),i.reposition(),o.timeOutValue=window.setTimeout(function(){this.removeElement(o)}.bind(this),this.options.duration),this},removeElement:function(t){t.className=t.className.replace(" on",""),window.setTimeout(function(){t.parentNode.removeChild(t),this.options.callback.call(t),i.reposition()}.bind(this),400)}},i.reposition=function(){for(var t,o={top:15,bottom:15},i={top:15,bottom:15},n={top:15,bottom:15},e=document.getElementsByClassName("toastify"),s=0;s<e.length;s++){t=!0===r(e[s],"top")?"top":"bottom";var a=e[s].offsetHeight;(0<window.innerWidth?window.innerWidth:screen.width)<=360?(e[s].style[t]=n[t]+"px",n[t]+=a+15):!0===r(e[s],"left")?(e[s].style[t]=o[t]+"px",o[t]+=a+15):(e[s].style[t]=i[t]+"px",i[t]+=a+15)}return this},i.lib.init.prototype=i.lib,i});
|
||||
//# sourceMappingURL=/sm/3f68e387be4f7a323a891120e4e01e3bee54a927113a386cf5e598b3cd442fcc.map
|
|
@ -1,56 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Simple Cloud Notifications</title>
|
||||
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
|
||||
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||
</div>
|
||||
|
||||
<div id="mainpnl">
|
||||
|
||||
<div class="fullcenterflex">
|
||||
|
||||
<?php if (isset($_GET['ok']) && $_GET['ok'] === "1" ): ?>
|
||||
|
||||
<a class="card success" href="/index.php?preset_user_id=<?php echo isset($_GET['preset_user_id'])?$_GET['preset_user_id']:'ERR';?>&preset_user_key=<?php echo isset($_GET['preset_user_key'])?$_GET['preset_user_key']:'ERR';?>">
|
||||
<div class="section">
|
||||
<h3 class="doc">Message sent</h3>
|
||||
<p class="doc">Message succesfully sent<br>
|
||||
<?php echo isset($_GET['quota_remain'])?$_GET['quota_remain']:'ERR';?>/<?php echo isset($_GET['quota_max'])?$_GET['quota_max']:'ERR';?> remaining</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<a class="card error" href="/index.php">
|
||||
<div class="section">
|
||||
<h3 class="doc">Failure</h3>
|
||||
<p class="doc">Unknown error</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
|
||||
|
||||
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
190
web/send.php
190
web/send.php
|
@ -1,190 +0,0 @@
|
|||
<?php
|
||||
|
||||
include_once 'api/model.php';
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') api_return(400, ['success' => false, 'error' => ERR::REQ_METHOD, 'errhighlight' => -1, 'message' => 'Invalid request method (must be POST)']);
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
if (!isset($INPUT['user_id'])) api_return(400, ['success' => false, 'error' => ERR::MISSING_UID, 'errhighlight' => 101, 'message' => 'Missing parameter [[user_id]]']);
|
||||
if (!isset($INPUT['user_key'])) api_return(400, ['success' => false, 'error' => ERR::MISSING_TOK, 'errhighlight' => 102, 'message' => 'Missing parameter [[user_token]]']);
|
||||
if (!isset($INPUT['title'])) api_return(400, ['success' => false, 'error' => ERR::MISSING_TITLE, 'errhighlight' => 103, 'message' => 'Missing parameter [[title]]']);
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
$message = $INPUT['title'];
|
||||
$content = isset($INPUT['content']) ? $INPUT['content'] : '';
|
||||
$priority = isset($INPUT['priority']) ? $INPUT['priority'] : '1';
|
||||
$usrmsgid = isset($INPUT['msg_id']) ? $INPUT['msg_id'] : null;
|
||||
$time = isset($INPUT['timestamp']) ? $INPUT['timestamp'] : time();
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
if (abs($time - time()) > 60*60*24*2) api_return(400, ['success' => false, 'error' => ERR::TIMESTAMP_OUT_OF_RANGE, 'errhighlight' => -1, 'message' => 'The timestamp mus be within 24 hours of now()']);
|
||||
|
||||
if ($priority !== '0' && $priority !== '1' && $priority !== '2') api_return(400, ['success' => false, 'error' => ERR::INVALID_PRIO, 'errhighlight' => 105, 'message' => 'Invalid priority']);
|
||||
|
||||
if (strlen(trim($message)) == 0) api_return(400, ['success' => false, 'error' => ERR::NO_TITLE, 'errhighlight' => 103, 'message' => 'No title specified']);
|
||||
if ($usrmsgid != null && strlen($usrmsgid) > 64) api_return(400, ['success' => false, 'error' => ERR::USR_MSG_ID_TOO_LONG, 'errhighlight' => -1, 'message' => 'MessageID too long (64 characters)']);
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, fcm_token, messages_sent, quota_today, is_pro, quota_day FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (count($datas)<=0) api_return(401, ['success' => false, 'error' => ERR::USER_NOT_FOUND, 'errhighlight' => 101, 'message' => 'User not found']);
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) api_return(401, ['success' => false, 'error' => ERR::USER_NOT_FOUND, 'errhighlight' => 101, 'message' => 'User not found']);
|
||||
if ($data['user_id'] !== (int)$user_id) api_return(401, ['success' => false, 'error' => ERR::USER_NOT_FOUND, 'errhighlight' => 101, 'message' => 'UserID not found']);
|
||||
if ($data['user_key'] !== $user_key) api_return(401, ['success' => false, 'error' => ERR::USER_AUTH_FAILED, 'errhighlight' => 102, 'message' => 'Authentification failed']);
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
if (strlen($message) > 120) api_return(400, ['success' => false, 'error' => ERR::TITLE_TOO_LONG, 'errhighlight' => 103, 'message' => 'Title too long (120 characters)']);
|
||||
if (strlen($content) > Statics::contentlen_max($data['is_pro'])) api_return(400, ['success' => false, 'error' => ERR::CONTENT_TOO_LONG, 'errhighlight' => 104, 'message' => 'Content too long ('.strlen($content).' characters; max := '.Statics::contentlen_max($data['is_pro']).' characters)']);
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
$fcm = $data['fcm_token'];
|
||||
|
||||
$new_quota = $data['quota_today'] + 1;
|
||||
if ($data['quota_day'] === null || $data['quota_day'] !== date("Y-m-d")) $new_quota=1;
|
||||
if ($new_quota > Statics::quota_max($data['is_pro'])) api_return(403, ['success' => false, 'error' => ERR::QUOTA_REACHED, 'errhighlight' => -1, 'message' => 'Daily quota reached ('.Statics::quota_max($data['is_pro']).')']);
|
||||
|
||||
if ($fcm == null || $fcm == '' || $fcm == false)
|
||||
{
|
||||
api_return(412, ['success' => false, 'error' => ERR::NO_DEVICE_LINKED, 'errhighlight' => -1, 'message' => 'No device linked with this account']);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
if ($usrmsgid != null)
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT scn_message_id FROM messages WHERE sender_user_id=:uid AND usr_message_id IS NOT NULL AND usr_message_id=:umid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id, 'umid' => $usrmsgid]);
|
||||
|
||||
if (count($stmt->fetchAll(PDO::FETCH_ASSOC))>0)
|
||||
{
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'message' => 'Message already sent',
|
||||
'suppress_send' => true,
|
||||
'response' => '',
|
||||
'messagecount' => $data['messages_sent']+1,
|
||||
'quota' => $data['quota_today'],
|
||||
'is_pro' => $data['is_pro'],
|
||||
'quota_max' => Statics::quota_max($data['is_pro']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO messages (sender_user_id, title, content, priority, sendtime, fcm_message_id, usr_message_id) VALUES (:suid, :t, :c, :p, :ts, :fmid, :umid)');
|
||||
$stmt->execute(
|
||||
[
|
||||
'suid' => $user_id,
|
||||
't' => $message,
|
||||
'c' => $content,
|
||||
'p' => $priority,
|
||||
'ts' => $time,
|
||||
'fmid' => null,
|
||||
'umid' => $usrmsgid,
|
||||
]);
|
||||
|
||||
$scn_msg_id = $pdo->lastInsertId();
|
||||
|
||||
$url = "https://fcm.googleapis.com/fcm/send";
|
||||
$payload = json_encode(
|
||||
[
|
||||
'to' => $fcm,
|
||||
//'dry_run' => true,
|
||||
'android' => [ 'priority' => 'high' ],
|
||||
//'notification' =>
|
||||
//[
|
||||
// 'title' => $message,
|
||||
// 'body' => $content,
|
||||
//],
|
||||
'data' =>
|
||||
[
|
||||
'title' => $message,
|
||||
'body' => str_limit($content, 1900),
|
||||
'trimmed' => (strlen($content) > 1900),
|
||||
'priority' => $priority,
|
||||
'timestamp' => $time,
|
||||
'usr_msg_id' => $usrmsgid,
|
||||
'scn_msg_id' => $scn_msg_id,
|
||||
]
|
||||
]);
|
||||
$header=
|
||||
[
|
||||
'Authorization' => 'key=' . getConfig()['firebase']['server_key'],
|
||||
'Content-Type' => 'application/json',
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$httpresult = sendPOST($url, $payload, $header);
|
||||
|
||||
if (try_json($httpresult, ['success']) != 1)
|
||||
{
|
||||
reportError("FCM communication failed (success_1 <> true)\n\n".$httpresult);
|
||||
$pdo->rollBack();
|
||||
api_return(500, ['success' => false, 'error' => ERR::FIREBASE_COM_ERRORED, 'errhighlight' => -1, 'message' => 'Communication with firebase service failed.']);
|
||||
}
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
reportError("FCM communication failed", $e);
|
||||
$pdo->rollBack();
|
||||
api_return(500, ['success' => false, 'error' => ERR::FIREBASE_COM_FAILED, 'errhighlight' => -1, 'message' => 'Communication with firebase service failed.'."\n\n".'Exception: ' . $e->getMessage()]);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), messages_sent=messages_sent+1, quota_today=:q, quota_day=NOW() WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $user_id, 'q' => $new_quota]);
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE messages SET fcm_message_id=:fmid WHERE scn_message_id=:smid');
|
||||
$stmt->execute([ 'fmid' => try_json($httpresult, ['results', 0, 'message_id']), 'smid' => $scn_msg_id ]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'error' => ERR::NO_ERROR,
|
||||
'errhighlight' => -1,
|
||||
'message' => 'Message sent',
|
||||
'suppress_send' => false,
|
||||
'response' => $httpresult,
|
||||
'messagecount' => $data['messages_sent']+1,
|
||||
'quota' => $new_quota,
|
||||
'is_pro' => $data['is_pro'],
|
||||
'quota_max' => Statics::quota_max($data['is_pro']),
|
||||
'scn_msg_id' => $scn_msg_id,
|
||||
]);
|
||||
}
|
||||
catch (Exception $mex)
|
||||
{
|
||||
reportError("Root try-catch triggered", $mex);
|
||||
if ($pdo !== null && $pdo->inTransaction()) $pdo->rollBack();
|
||||
api_return(500, ['success' => false, 'error' => ERR::INTERNAL_EXCEPTION, 'errhighlight' => -1, 'message' => 'PHP script threw exception.'."\n\n".'Exception: ' . $e->getMessage()]);
|
||||
}
|
Loading…
Reference in New Issue