17 Commits

Author SHA1 Message Date
5b381eb3b9 Init react-native project 2023-11-18 19:34:08 +01:00
9f3e183d72 Update goext to v0.0.291 2023-10-26 13:14:11 +02:00
51f5f1005a Switch to new Swaggo Makefile template
All checks were successful
Build Docker and Deploy / Build Docker Container (push) Successful in 1m57s
Build Docker and Deploy / Deploy to Server (push) Successful in 12s
2023-10-17 16:47:50 +02:00
0a380f861e Add scn_send.sh to repo 2023-10-14 21:37:00 +02:00
b712ad3488 Use better go guard in Makefile::clean
All checks were successful
Build Docker and Deploy / Build Docker Container (push) Successful in 3m44s
Build Docker and Deploy / Deploy to Server (push) Successful in 12s
2023-08-16 09:48:28 +02:00
9f656bdefe Refactor message sending into logic package (+ more tests for uptime-kuma)
All checks were successful
Build Docker and Deploy / Build Docker Container (push) Successful in 1m33s
Build Docker and Deploy / Deploy to Server (push) Successful in 7s
2023-08-12 19:07:39 +02:00
a4a651229c Added gitea-actions workflow
All checks were successful
Build Docker and Deploy / Build Docker Container (push) Successful in 1m43s
Build Docker and Deploy / Deploy to Server (push) Successful in 7s
2023-08-12 15:51:14 +02:00
4773800f23 remove old PHP project 2023-08-12 11:14:32 +02:00
bef0b8189e uptime kuma webhook endpoint 2023-08-12 11:14:32 +02:00
674714f0f3 Return more data in /users/{uid} 2023-07-30 16:53:46 +02:00
ee9e858584 Increase pro quota and bodysize 2023-07-30 16:37:39 +02:00
165c6d8614 Refactor API of /api/v2/users/{uid}/subscriptions 2023-07-30 15:58:37 +02:00
8a6719fc19 Remove message.owner_user_id field and implement db migrations 2023-07-27 17:44:06 +02:00
308361a834 Prevent deleting messages of subscribed-only channels 2023-07-27 15:23:56 +02:00
44df964f6f todos 2023-07-04 11:31:52 +02:00
56bf266919 fix scn_send script with non-urlencoded data 2023-06-26 14:49:14 +02:00
f3658d6636 fix wrong data in compat_ids (requery.php) 2023-06-23 11:50:18 +02:00
109 changed files with 17789 additions and 3510 deletions

View File

@@ -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

35
reactnative/.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo

83
reactnative/App.tsx Normal file
View File

@@ -0,0 +1,83 @@
import { FlashList } from '@shopify/flash-list';
import { StatusBar } from 'expo-status-bar';
import { useRef, useState } from 'react';
import { LayoutAnimation, Pressable, StyleSheet, Text, View } from 'react-native';
export default function App() {
const generateArray = (size: number) => {
const arr = new Array(size);
for (let i = 0; i < size; i++) {
arr[i] = i;
}
return arr;
};
const [refreshing, setRefreshing] = useState(false);
const [data, setData] = useState(generateArray(100));
const list = useRef<FlashList<number> | null>(null);
const removeItem = (item: number) => {
setData(
data.filter((dataItem) => {
return dataItem !== item;
})
);
list.current?.prepareForLayoutAnimationRender();
// after removing the item, we start animation
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
};
const renderItem = ({ item }: { item: number }) => {
const backgroundColor = item % 2 === 0 ? "#00a1f1" : "#ffbb00";
return (
<Pressable
onPress={() => {
removeItem(item);
}}
>
<View
style={{
...styles.container,
backgroundColor: item > 97 ? "red" : backgroundColor,
height: item % 2 === 0 ? 100 : 200,
}}
>
<Text>Cell Id: {item}</Text>
</View>
</Pressable>
);
};
return (
<FlashList
ref={list}
style={{flexGrow: 1, width: '100%', height: 50, backgroundColor: '#88F'}}
refreshing={refreshing}
onRefresh={() => {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}}
keyExtractor={(item: number) => {
return item.toString();
}}
getItemType={(item: number) => {
return item > 97 ? "even" : "odd";
}}
renderItem={renderItem}
estimatedItemSize={100}
data={data}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f',
alignItems: 'center',
justifyContent: 'center',
},
});

4
reactnative/Makefile Normal file
View File

@@ -0,0 +1,4 @@
run:
npx expo start

30
reactnative/app.json Normal file
View File

@@ -0,0 +1,30 @@
{
"expo": {
"name": "simplecloudnotifier",
"slug": "simplecloudnotifier",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
reactnative/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

13710
reactnative/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
reactnative/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "simplecloudnotifier",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"expo": "~49.0.10",
"expo-status-bar": "~1.6.0",
"react": "18.2.0",
"react-native": "0.72.4",
"@shopify/flash-list": "1.4.3"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.2.14",
"typescript": "^5.1.3"
},
"private": true
}

View File

@@ -0,0 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}

View File

@@ -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

View File

@@ -18,6 +18,10 @@ identifier.sqlite
.idea/dataSources.xml
.swaggobin
scn_send.sh
##############

6
scnserver/.idea/golinter.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoLinterSettings">
<option name="checkGoLinterExe" value="false" />
</component>
</project>

View File

@@ -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

View File

@@ -7,12 +7,20 @@ HASH=$(shell git rev-parse HEAD)
.PHONY: test swagger pygmentize docker migrate dgi pygmentize lint docker
build: swagger pygmentize fmt
SWAGGO_VERSION=v1.8.12
SWAGGO=github.com/swaggo/swag/cmd/swag@$(SWAGGO_VERSION)
build: ids enums swagger pygmentize fmt
mkdir -p _build
rm -f ./_build/scn_backend
go generate ./...
CGO_ENABLED=1 go build -v -o _build/scn_backend -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" ./cmd/scnserver
enums:
go generate models/enums.go
ids:
go generate models/ids.go
run: build
mkdir -p .run-data
_build/scn_backend
@@ -30,6 +38,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,15 +47,19 @@ 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"
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"
@@ -67,7 +80,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 +88,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

View File

@@ -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...

View File

@@ -0,0 +1,20 @@
package main
import (
"gogs.mikescher.com/BlackForestBytes/goext/bfcodegen"
"os"
)
func main() {
dest := os.Args[2]
wd, err := os.Getwd()
if err != nil {
panic(err)
}
err = bfcodegen.GenerateCharsetIDSpecs(wd, dest)
if err != nil {
panic(err)
}
}

View File

@@ -6,6 +6,7 @@ import (
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
@@ -146,7 +147,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 {
@@ -206,7 +207,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 {
@@ -298,7 +299,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 +307,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 +349,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 +421,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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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"))
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)
}
} 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)
}
} 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)
}
} else if sel == "incoming_all" {
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)
}
filter := models.SubscriptionFilter{}
filter.AnyUserID = langext.Ptr(u.UserID)
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)
}
}
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
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)
}
}
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)
}
}
if q.SubscriberUserID != nil {
filter.SubscriberUserID2 = langext.Ptr([]models.UserID{*q.SubscriberUserID})
}
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 {

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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,
}))
}

View File

@@ -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
}

View File

@@ -21,6 +21,7 @@ type Router struct {
websiteHandler handler.WebsiteHandler
apiHandler handler.APIHandler
messageHandler handler.MessageHandler
externalHandler handler.ExternalHandler
}
func NewRouter(app *logic.Application) *Router {
@@ -32,6 +33,7 @@ func NewRouter(app *logic.Application) *Router {
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))
}
// ================

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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,7 @@ 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) {
func (db *Database) CreateChannel(ctx db.TxContext, userid models.UserID, dispName string, intName string, subscribeKey string) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Channel{}, err
@@ -81,7 +83,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 +107,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 +137,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 +168,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 +202,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 +224,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 +241,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 +258,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

View File

@@ -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

View File

@@ -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

View File

@@ -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
currschema = schema.PrimarySchemaVersion
}
err = db.pp.Init(ctx) // Re-Init
if err != nil {
return err
}
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
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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package logic
package simplectx
import (
"blackforestbytes.com/simplecloudnotifier/db"
@@ -51,7 +51,9 @@ func (sc *SimpleContext) Cancel() {
}
sc.transaction = nil
}
if sc.cancelFunc != nil {
sc.cancelFunc()
}
}
func (sc *SimpleContext) GetOrCreateTransaction(db db.DatabaseImpl) (sq.Tx, error) {

View File

@@ -4,19 +4,20 @@ go 1.19
require (
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.14.1
github.com/go-playground/validator/v10 v10.15.5
github.com/go-sql-driver/mysql v1.7.1
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
github.com/rs/zerolog v1.31.0
gogs.mikescher.com/BlackForestBytes/goext v0.0.291
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/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/bytedance/sonic v1.10.2 // 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.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -26,18 +27,20 @@ require (
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // 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.1.0 // indirect
github.com/rs/xid v1.5.0 // 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
go.mongodb.org/mongo-driver v1.12.1 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,15 +1,19 @@
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.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
@@ -19,8 +23,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
@@ -28,6 +32,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -36,20 +42,21 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
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=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
@@ -58,14 +65,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/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=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -75,36 +84,70 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
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=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
gogs.mikescher.com/BlackForestBytes/goext v0.0.291 h1:yiRHw8wV9LEZUKljvQ2MBHgeSAI6ZzajDS+FJRGnzVc=
gogs.mikescher.com/BlackForestBytes/goext v0.0.291/go.mod h1:TtMY+/pv09fOldLO3noZ6Yfcy9KU9MjVQ0KQ9nraTeI=
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/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
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/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
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/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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 +155,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=

View File

@@ -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()

View File

@@ -4,6 +4,7 @@ import (
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/google"
"blackforestbytes.com/simplecloudnotifier/models"
"blackforestbytes.com/simplecloudnotifier/push"
@@ -164,14 +165,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 +285,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) {

222
scnserver/logic/message.go Normal file
View File

@@ -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
}

View File

@@ -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 {

View File

@@ -0,0 +1,3 @@
package models
//go:generate go run ../_gen/enum-generate.go -- enums_gen.go

View File

@@ -3,31 +3,9 @@
package models
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.com/BlackForestBytes/goext/enums"
const ChecksumGenerator = "4bfd61daa179e1452035a34c25c6f8170a08500bc0a7aa0e3981f95ad4b0d7d2"
type Enum interface {
Valid() bool
ValuesAny() []any
ValuesMeta() []EnumMetaValue
VarName() string
}
type StringEnum interface {
Enum
String() string
}
type DescriptionEnum interface {
Enum
Description() string
}
type EnumMetaValue struct {
VarName string `json:"varName"`
Value any `json:"value"`
Description *string `json:"description"`
}
const ChecksumEnumGenerator = "8926e4a9845e67086109bef7ca447371ab5c0a94fcfd988f14fd4ee98da9e932" // GoExtVersion: 0.0.291
// ================================ ClientType ================================
//
@@ -58,11 +36,8 @@ func (e ClientType) ValuesAny() []any {
return langext.ArrCastToAny(__ClientTypeValues)
}
func (e ClientType) ValuesMeta() []EnumMetaValue {
return []EnumMetaValue{
EnumMetaValue{VarName: "ClientTypeAndroid", Value: ClientTypeAndroid, Description: nil},
EnumMetaValue{VarName: "ClientTypeIOS", Value: ClientTypeIOS, Description: nil},
}
func (e ClientType) ValuesMeta() []enums.EnumMetaValue {
return ClientTypeValuesMeta()
}
func (e ClientType) String() string {
@@ -76,6 +51,10 @@ func (e ClientType) VarName() string {
return ""
}
func (e ClientType) Meta() enums.EnumMetaValue {
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
}
func ParseClientType(vv string) (ClientType, bool) {
for _, ev := range __ClientTypeValues {
if string(ev) == vv {
@@ -89,10 +68,10 @@ func ClientTypeValues() []ClientType {
return __ClientTypeValues
}
func ClientTypeValuesMeta() []EnumMetaValue {
return []EnumMetaValue{
EnumMetaValue{VarName: "ClientTypeAndroid", Value: ClientTypeAndroid, Description: nil},
EnumMetaValue{VarName: "ClientTypeIOS", Value: ClientTypeIOS, Description: nil},
func ClientTypeValuesMeta() []enums.EnumMetaValue {
return []enums.EnumMetaValue{
ClientTypeAndroid.Meta(),
ClientTypeIOS.Meta(),
}
}
@@ -127,12 +106,8 @@ func (e DeliveryStatus) ValuesAny() []any {
return langext.ArrCastToAny(__DeliveryStatusValues)
}
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},
}
func (e DeliveryStatus) ValuesMeta() []enums.EnumMetaValue {
return DeliveryStatusValuesMeta()
}
func (e DeliveryStatus) String() string {
@@ -146,6 +121,10 @@ func (e DeliveryStatus) VarName() string {
return ""
}
func (e DeliveryStatus) Meta() enums.EnumMetaValue {
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
}
func ParseDeliveryStatus(vv string) (DeliveryStatus, bool) {
for _, ev := range __DeliveryStatusValues {
if string(ev) == vv {
@@ -159,11 +138,11 @@ func DeliveryStatusValues() []DeliveryStatus {
return __DeliveryStatusValues
}
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},
func DeliveryStatusValuesMeta() []enums.EnumMetaValue {
return []enums.EnumMetaValue{
DeliveryStatusRetry.Meta(),
DeliveryStatusSuccess.Meta(),
DeliveryStatusFailed.Meta(),
}
}
@@ -207,13 +186,8 @@ func (e TokenPerm) ValuesAny() []any {
return langext.ArrCastToAny(__TokenPermValues)
}
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")},
}
func (e TokenPerm) ValuesMeta() []enums.EnumMetaValue {
return TokenPermValuesMeta()
}
func (e TokenPerm) String() string {
@@ -234,6 +208,10 @@ func (e TokenPerm) VarName() string {
return ""
}
func (e TokenPerm) Meta() enums.EnumMetaValue {
return enums.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 {
@@ -247,11 +225,11 @@ func TokenPermValues() []TokenPerm {
return __TokenPermValues
}
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")},
func TokenPermValuesMeta() []enums.EnumMetaValue {
return []enums.EnumMetaValue{
PermAdmin.Meta(),
PermChannelRead.Meta(),
PermChannelSend.Meta(),
PermUserRead.Meta(),
}
}

View File

@@ -1,19 +1,11 @@
package models
import (
"crypto/rand"
"errors"
"fmt"
"github.com/go-playground/validator/v10"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"math/big"
"reflect"
"regexp"
"strings"
)
//go:generate go run ../_gen/id-generate.go -- ids_gen.go
type EntityID interface {
String() string
Valid() error
@@ -23,389 +15,11 @@ type EntityID interface {
Regex() rext.Regex
}
const idlen = 24
const checklen = 1
const idCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const idCharsetLen = len(idCharset)
var charSetReverseMap = generateCharsetMap()
const (
prefixUserID = "USR"
prefixChannelID = "CHA"
prefixDeliveryID = "DEL"
prefixMessageID = "MSG"
prefixSubscriptionID = "SUB"
prefixClientID = "CLN"
prefixRequestID = "REQ"
prefixKeyTokenID = "TOK"
)
var (
regexUserID = generateRegex(prefixUserID)
regexChannelID = generateRegex(prefixChannelID)
regexDeliveryID = generateRegex(prefixDeliveryID)
regexMessageID = generateRegex(prefixMessageID)
regexSubscriptionID = generateRegex(prefixSubscriptionID)
regexClientID = generateRegex(prefixClientID)
regexRequestID = generateRegex(prefixRequestID)
regexKeyTokenID = generateRegex(prefixKeyTokenID)
)
func generateRegex(prefix string) rext.Regex {
return rext.W(regexp.MustCompile(fmt.Sprintf("^%s[%s]{%d}[%s]{%d}$", prefix, idCharset, idlen-len(prefix)-checklen, idCharset, checklen)))
}
func generateCharsetMap() []int {
result := make([]int, 128)
for i := 0; i < len(result); i++ {
result[i] = -1
}
for idx, chr := range idCharset {
result[int(chr)] = idx
}
return result
}
func generateID(prefix string) string {
k := ""
max := big.NewInt(int64(idCharsetLen))
checksum := 0
for i := 0; i < idlen-len(prefix)-checklen; i++ {
v, err := rand.Int(rand.Reader, max)
if err != nil {
panic(err)
}
v64 := v.Int64()
k += string(idCharset[v64])
checksum = (checksum + int(v64)) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
return prefix + k + checkstr
}
func validateID(prefix string, value string) error {
if len(value) != idlen {
return errors.New("id has the wrong length")
}
if !strings.HasPrefix(value, prefix) {
return errors.New("id is missing the correct prefix")
}
checksum := 0
for i := len(prefix); i < len(value)-checklen; i++ {
ichr := int(value[i])
if ichr < 0 || ichr >= len(charSetReverseMap) || charSetReverseMap[ichr] == -1 {
return errors.New("id contains invalid characters")
}
checksum = (checksum + charSetReverseMap[ichr]) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
if !strings.HasSuffix(value, checkstr) {
return errors.New("id checkstring is invalid")
}
return nil
}
func getRawData(prefix string, value string) string {
if len(value) != idlen {
return ""
}
return value[len(prefix) : idlen-checklen]
}
func getCheckString(prefix string, value string) string {
if len(value) != idlen {
return ""
}
return value[idlen-checklen:]
}
func ValidateEntityID(vfl validator.FieldLevel) bool {
if !vfl.Field().CanInterface() {
log.Error().Msgf("Failed to validate EntityID (cannot interface ?!?)")
return false
}
ifvalue := vfl.Field().Interface()
if value1, ok := ifvalue.(EntityID); ok {
if vfl.Field().Type().Kind() == reflect.Pointer && langext.IsNil(value1) {
return true
}
if err := value1.Valid(); err != nil {
log.Debug().Msgf("Failed to validate EntityID '%s' (%s)", value1.String(), err.Error())
return false
} else {
return true
}
} else {
log.Error().Msgf("Failed to validate EntityID (wrong type: %T)", ifvalue)
return false
}
}
// ------------------------------------------------------------
type UserID string
func NewUserID() UserID {
return UserID(generateID(prefixUserID))
}
func (id UserID) Valid() error {
return validateID(prefixUserID, string(id))
}
func (id UserID) String() string {
return string(id)
}
func (id UserID) Prefix() string {
return prefixUserID
}
func (id UserID) Raw() string {
return getRawData(prefixUserID, string(id))
}
func (id UserID) CheckString() string {
return getCheckString(prefixUserID, string(id))
}
func (id UserID) Regex() rext.Regex {
return regexUserID
}
// ------------------------------------------------------------
type ChannelID string
func NewChannelID() ChannelID {
return ChannelID(generateID(prefixChannelID))
}
func (id ChannelID) Valid() error {
return validateID(prefixChannelID, string(id))
}
func (id ChannelID) String() string {
return string(id)
}
func (id ChannelID) Prefix() string {
return prefixChannelID
}
func (id ChannelID) Raw() string {
return getRawData(prefixChannelID, string(id))
}
func (id ChannelID) CheckString() string {
return getCheckString(prefixChannelID, string(id))
}
func (id ChannelID) Regex() rext.Regex {
return regexChannelID
}
// ------------------------------------------------------------
type DeliveryID string
func NewDeliveryID() DeliveryID {
return DeliveryID(generateID(prefixDeliveryID))
}
func (id DeliveryID) Valid() error {
return validateID(prefixDeliveryID, string(id))
}
func (id DeliveryID) String() string {
return string(id)
}
func (id DeliveryID) Prefix() string {
return prefixDeliveryID
}
func (id DeliveryID) Raw() string {
return getRawData(prefixDeliveryID, string(id))
}
func (id DeliveryID) CheckString() string {
return getCheckString(prefixDeliveryID, string(id))
}
func (id DeliveryID) Regex() rext.Regex {
return regexDeliveryID
}
// ------------------------------------------------------------
type MessageID string
func NewMessageID() MessageID {
return MessageID(generateID(prefixMessageID))
}
func (id MessageID) Valid() error {
return validateID(prefixMessageID, string(id))
}
func (id MessageID) String() string {
return string(id)
}
func (id MessageID) Prefix() string {
return prefixMessageID
}
func (id MessageID) Raw() string {
return getRawData(prefixMessageID, string(id))
}
func (id MessageID) CheckString() string {
return getCheckString(prefixMessageID, string(id))
}
func (id MessageID) Regex() rext.Regex {
return regexMessageID
}
// ------------------------------------------------------------
type SubscriptionID string
func NewSubscriptionID() SubscriptionID {
return SubscriptionID(generateID(prefixSubscriptionID))
}
func (id SubscriptionID) Valid() error {
return validateID(prefixSubscriptionID, string(id))
}
func (id SubscriptionID) String() string {
return string(id)
}
func (id SubscriptionID) Prefix() string {
return prefixSubscriptionID
}
func (id SubscriptionID) Raw() string {
return getRawData(prefixSubscriptionID, string(id))
}
func (id SubscriptionID) CheckString() string {
return getCheckString(prefixSubscriptionID, string(id))
}
func (id SubscriptionID) Regex() rext.Regex {
return regexSubscriptionID
}
// ------------------------------------------------------------
type ClientID string
func NewClientID() ClientID {
return ClientID(generateID(prefixClientID))
}
func (id ClientID) Valid() error {
return validateID(prefixClientID, string(id))
}
func (id ClientID) String() string {
return string(id)
}
func (id ClientID) Prefix() string {
return prefixClientID
}
func (id ClientID) Raw() string {
return getRawData(prefixClientID, string(id))
}
func (id ClientID) CheckString() string {
return getCheckString(prefixClientID, string(id))
}
func (id ClientID) Regex() rext.Regex {
return regexClientID
}
// ------------------------------------------------------------
type RequestID string
func NewRequestID() RequestID {
return RequestID(generateID(prefixRequestID))
}
func (id RequestID) Valid() error {
return validateID(prefixRequestID, string(id))
}
func (id RequestID) String() string {
return string(id)
}
func (id RequestID) Prefix() string {
return prefixRequestID
}
func (id RequestID) Raw() string {
return getRawData(prefixRequestID, string(id))
}
func (id RequestID) CheckString() string {
return getCheckString(prefixRequestID, string(id))
}
func (id RequestID) Regex() rext.Regex {
return regexRequestID
}
// ------------------------------------------------------------
type KeyTokenID string
func NewKeyTokenID() KeyTokenID {
return KeyTokenID(generateID(prefixKeyTokenID))
}
func (id KeyTokenID) Valid() error {
return validateID(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) String() string {
return string(id)
}
func (id KeyTokenID) Prefix() string {
return prefixKeyTokenID
}
func (id KeyTokenID) Raw() string {
return getRawData(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) CheckString() string {
return getCheckString(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) Regex() rext.Regex {
return regexKeyTokenID
}
type UserID string //@csid:type [USR]
type ChannelID string //@csid:type [CHA]
type DeliveryID string //@csid:type [DEL]
type MessageID string //@csid:type [MSG]
type SubscriptionID string //@csid:type [SUB]
type ClientID string //@csid:type [CLN]
type RequestID string //@csid:type [REQ]
type KeyTokenID string //@csid:type [TOK]

388
scnserver/models/ids_gen.go Normal file
View File

@@ -0,0 +1,388 @@
// Code generated by id-generate.go DO NOT EDIT.
package models
import "crypto/rand"
import "fmt"
import "github.com/go-playground/validator/v10"
import "github.com/rs/zerolog/log"
import "gogs.mikescher.com/BlackForestBytes/goext/exerr"
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.com/BlackForestBytes/goext/rext"
import "math/big"
import "reflect"
import "regexp"
import "strings"
const ChecksumCharsetIDGenerator = "8926e4a9845e67086109bef7ca447371ab5c0a94fcfd988f14fd4ee98da9e932" // GoExtVersion: 0.0.291
const idlen = 24
const checklen = 1
const idCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const idCharsetLen = len(idCharset)
var charSetReverseMap = generateCharsetMap()
const (
prefixUserID = "USR"
prefixChannelID = "CHA"
prefixDeliveryID = "DEL"
prefixMessageID = "MSG"
prefixSubscriptionID = "SUB"
prefixClientID = "CLN"
prefixRequestID = "REQ"
prefixKeyTokenID = "TOK"
)
var (
regexUserID = generateRegex(prefixUserID)
regexChannelID = generateRegex(prefixChannelID)
regexDeliveryID = generateRegex(prefixDeliveryID)
regexMessageID = generateRegex(prefixMessageID)
regexSubscriptionID = generateRegex(prefixSubscriptionID)
regexClientID = generateRegex(prefixClientID)
regexRequestID = generateRegex(prefixRequestID)
regexKeyTokenID = generateRegex(prefixKeyTokenID)
)
func generateRegex(prefix string) rext.Regex {
return rext.W(regexp.MustCompile(fmt.Sprintf("^%s[%s]{%d}[%s]{%d}$", prefix, idCharset, idlen-len(prefix)-checklen, idCharset, checklen)))
}
func generateCharsetMap() []int {
result := make([]int, 128)
for i := 0; i < len(result); i++ {
result[i] = -1
}
for idx, chr := range idCharset {
result[int(chr)] = idx
}
return result
}
func generateID(prefix string) string {
k := ""
max := big.NewInt(int64(idCharsetLen))
checksum := 0
for i := 0; i < idlen-len(prefix)-checklen; i++ {
v, err := rand.Int(rand.Reader, max)
if err != nil {
panic(err)
}
v64 := v.Int64()
k += string(idCharset[v64])
checksum = (checksum + int(v64)) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
return prefix + k + checkstr
}
func validateID(prefix string, value string) error {
if len(value) != idlen {
return exerr.New(exerr.TypeInvalidCSID, "id has the wrong length").Str("value", value).Build()
}
if !strings.HasPrefix(value, prefix) {
return exerr.New(exerr.TypeInvalidCSID, "id is missing the correct prefix").Str("value", value).Str("prefix", prefix).Build()
}
checksum := 0
for i := len(prefix); i < len(value)-checklen; i++ {
ichr := int(value[i])
if ichr < 0 || ichr >= len(charSetReverseMap) || charSetReverseMap[ichr] == -1 {
return exerr.New(exerr.TypeInvalidCSID, "id contains invalid characters").Str("value", value).Build()
}
checksum = (checksum + charSetReverseMap[ichr]) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
if !strings.HasSuffix(value, checkstr) {
return exerr.New(exerr.TypeInvalidCSID, "id checkstring is invalid").Str("value", value).Str("checkstr", checkstr).Build()
}
return nil
}
func getRawData(prefix string, value string) string {
if len(value) != idlen {
return ""
}
return value[len(prefix) : idlen-checklen]
}
func getCheckString(prefix string, value string) string {
if len(value) != idlen {
return ""
}
return value[idlen-checklen:]
}
func ValidateEntityID(vfl validator.FieldLevel) bool {
if !vfl.Field().CanInterface() {
log.Error().Msgf("Failed to validate EntityID (cannot interface ?!?)")
return false
}
ifvalue := vfl.Field().Interface()
if value1, ok := ifvalue.(EntityID); ok {
if vfl.Field().Type().Kind() == reflect.Pointer && langext.IsNil(value1) {
return true
}
if err := value1.Valid(); err != nil {
log.Debug().Msgf("Failed to validate EntityID '%s' (%s)", value1.String(), err.Error())
return false
} else {
return true
}
} else {
log.Error().Msgf("Failed to validate EntityID (wrong type: %T)", ifvalue)
return false
}
}
// ================================ UserID (ids.go) ================================
func NewUserID() UserID {
return UserID(generateID(prefixUserID))
}
func (id UserID) Valid() error {
return validateID(prefixUserID, string(id))
}
func (i UserID) String() string {
return string(i)
}
func (i UserID) Prefix() string {
return prefixUserID
}
func (id UserID) Raw() string {
return getRawData(prefixUserID, string(id))
}
func (id UserID) CheckString() string {
return getCheckString(prefixUserID, string(id))
}
func (id UserID) Regex() rext.Regex {
return regexUserID
}
// ================================ ChannelID (ids.go) ================================
func NewChannelID() ChannelID {
return ChannelID(generateID(prefixChannelID))
}
func (id ChannelID) Valid() error {
return validateID(prefixChannelID, string(id))
}
func (i ChannelID) String() string {
return string(i)
}
func (i ChannelID) Prefix() string {
return prefixChannelID
}
func (id ChannelID) Raw() string {
return getRawData(prefixChannelID, string(id))
}
func (id ChannelID) CheckString() string {
return getCheckString(prefixChannelID, string(id))
}
func (id ChannelID) Regex() rext.Regex {
return regexChannelID
}
// ================================ DeliveryID (ids.go) ================================
func NewDeliveryID() DeliveryID {
return DeliveryID(generateID(prefixDeliveryID))
}
func (id DeliveryID) Valid() error {
return validateID(prefixDeliveryID, string(id))
}
func (i DeliveryID) String() string {
return string(i)
}
func (i DeliveryID) Prefix() string {
return prefixDeliveryID
}
func (id DeliveryID) Raw() string {
return getRawData(prefixDeliveryID, string(id))
}
func (id DeliveryID) CheckString() string {
return getCheckString(prefixDeliveryID, string(id))
}
func (id DeliveryID) Regex() rext.Regex {
return regexDeliveryID
}
// ================================ MessageID (ids.go) ================================
func NewMessageID() MessageID {
return MessageID(generateID(prefixMessageID))
}
func (id MessageID) Valid() error {
return validateID(prefixMessageID, string(id))
}
func (i MessageID) String() string {
return string(i)
}
func (i MessageID) Prefix() string {
return prefixMessageID
}
func (id MessageID) Raw() string {
return getRawData(prefixMessageID, string(id))
}
func (id MessageID) CheckString() string {
return getCheckString(prefixMessageID, string(id))
}
func (id MessageID) Regex() rext.Regex {
return regexMessageID
}
// ================================ SubscriptionID (ids.go) ================================
func NewSubscriptionID() SubscriptionID {
return SubscriptionID(generateID(prefixSubscriptionID))
}
func (id SubscriptionID) Valid() error {
return validateID(prefixSubscriptionID, string(id))
}
func (i SubscriptionID) String() string {
return string(i)
}
func (i SubscriptionID) Prefix() string {
return prefixSubscriptionID
}
func (id SubscriptionID) Raw() string {
return getRawData(prefixSubscriptionID, string(id))
}
func (id SubscriptionID) CheckString() string {
return getCheckString(prefixSubscriptionID, string(id))
}
func (id SubscriptionID) Regex() rext.Regex {
return regexSubscriptionID
}
// ================================ ClientID (ids.go) ================================
func NewClientID() ClientID {
return ClientID(generateID(prefixClientID))
}
func (id ClientID) Valid() error {
return validateID(prefixClientID, string(id))
}
func (i ClientID) String() string {
return string(i)
}
func (i ClientID) Prefix() string {
return prefixClientID
}
func (id ClientID) Raw() string {
return getRawData(prefixClientID, string(id))
}
func (id ClientID) CheckString() string {
return getCheckString(prefixClientID, string(id))
}
func (id ClientID) Regex() rext.Regex {
return regexClientID
}
// ================================ RequestID (ids.go) ================================
func NewRequestID() RequestID {
return RequestID(generateID(prefixRequestID))
}
func (id RequestID) Valid() error {
return validateID(prefixRequestID, string(id))
}
func (i RequestID) String() string {
return string(i)
}
func (i RequestID) Prefix() string {
return prefixRequestID
}
func (id RequestID) Raw() string {
return getRawData(prefixRequestID, string(id))
}
func (id RequestID) CheckString() string {
return getCheckString(prefixRequestID, string(id))
}
func (id RequestID) Regex() rext.Regex {
return regexRequestID
}
// ================================ KeyTokenID (ids.go) ================================
func NewKeyTokenID() KeyTokenID {
return KeyTokenID(generateID(prefixKeyTokenID))
}
func (id KeyTokenID) Valid() error {
return validateID(prefixKeyTokenID, string(id))
}
func (i KeyTokenID) String() string {
return string(i)
}
func (i KeyTokenID) Prefix() string {
return prefixKeyTokenID
}
func (id KeyTokenID) Raw() string {
return getRawData(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) CheckString() string {
return getCheckString(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) Regex() rext.Regex {
return regexKeyTokenID
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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))]
}

View File

@@ -34,6 +34,13 @@ func (u User) JSON() UserJSON {
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
}
@@ -120,6 +127,13 @@ type UserJSON struct {
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 {

View File

@@ -5,8 +5,6 @@ import (
"time"
)
//go:generate go run ../_gen/enum-generate.go -- enums_gen.go
func timeOptFmt(t *time.Time, fmt string) *string {
if t == nil {
return nil

View File

@@ -958,13 +958,13 @@
}
},
"/api/v2/messages/{mid}": {
"delete": {
"description": "The user must own the message and request the resource with the ADMIN Key",
"get": {
"description": "The user must either own the message and request the resource with the READ or ADMIN Key\nOr the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key\nThe returned message is never trimmed",
"tags": [
"API-v2"
],
"summary": "Delete a single message",
"operationId": "api-messages-delete",
"summary": "Get a single message (untrimmed)",
"operationId": "api-messages-get",
"parameters": [
{
"type": "string",
@@ -1007,13 +1007,13 @@
}
}
},
"patch": {
"description": "The user must either own the message and request the resource with the READ or ADMIN Key\nOr the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key\nThe returned message is never trimmed",
"delete": {
"description": "The user must own the message and request the resource with the ADMIN Key",
"tags": [
"API-v2"
],
"summary": "Get a single message (untrimmed)",
"operationId": "api-messages-get",
"summary": "Delete a single message",
"operationId": "api-messages-delete",
"parameters": [
{
"type": "string",
@@ -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"
],
@@ -2449,6 +2449,104 @@
}
}
},
"/external/v1/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",
"name": "channel",
"in": "query"
},
{
"type": "string",
"name": "channel_down",
"in": "query"
},
{
"type": "string",
"name": "channel_up",
"in": "query"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "integer",
"name": "priority",
"in": "query"
},
{
"type": "integer",
"name": "priority_down",
"in": "query"
},
{
"type": "integer",
"name": "priority_up",
"in": "query"
},
{
"type": "string",
"name": "senderName",
"in": "query"
},
{
"type": "string",
"example": "7725",
"name": "user_id",
"in": "query"
},
{
"description": " ",
"name": "post_body",
"in": "body",
"schema": {
"$ref": "#/definitions/handler.UptimeKuma.body"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.UptimeKuma.response"
}
},
"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"
}
}
}
}
},
"/send": {
"post": {
"description": "All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required",
@@ -2629,72 +2727,120 @@
"parameters": [
{
"type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content",
"in": "query"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id",
"in": "query"
},
{
"enum": [
0,
1,
2
],
"type": "integer",
"example": 1,
"name": "priority",
"in": "query"
},
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{
"type": "number",
"example": 1669824037,
"name": "timestamp",
"in": "query"
},
{
"type": "string",
"example": "Hello World",
"name": "title",
"in": "query"
},
{
"type": "integer",
"type": "string",
"example": "7725",
"name": "user_id",
"in": "query"
},
{
"type": "string",
"name": "user_key",
"in": "query"
"example": "test",
"name": "channel",
"in": "formData"
},
{
"type": "string",
"example": "This is a message",
"name": "content",
"in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id",
"in": "formData"
},
{
"enum": [
0,
1,
2
],
"type": "integer",
"example": 1,
"name": "priority",
"in": "formData"
},
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{
"type": "number",
"example": 1669824037,
"name": "timestamp",
"in": "formData"
},
{
"type": "string",
"example": "Hello World",
"name": "title",
"in": "formData"
},
{
"type": "integer",
"name": "user_id",
"in": "formData"
},
{
"type": "string",
"name": "user_key",
"example": "7725",
"name": "user_id",
"in": "formData"
}
],
@@ -2702,7 +2848,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.SendMessageCompat.response"
"$ref": "#/definitions/handler.SendMessage.response"
}
},
"400": {
@@ -2952,8 +3098,6 @@
"handler.CreateUserKey.body": {
"type": "object",
"required": [
"all_channels",
"channels",
"name",
"permissions"
],
@@ -3262,41 +3406,6 @@
}
}
},
"handler.SendMessageCompat.response": {
"type": "object",
"properties": {
"errhighlight": {
"type": "integer"
},
"error": {
"$ref": "#/definitions/apierr.APIError"
},
"is_pro": {
"type": "boolean"
},
"message": {
"type": "string"
},
"messagecount": {
"type": "integer"
},
"quota": {
"type": "integer"
},
"quota_max": {
"type": "integer"
},
"scn_msg_id": {
"type": "integer"
},
"success": {
"type": "boolean"
},
"suppress_send": {
"type": "boolean"
}
}
},
"handler.Sleep.response": {
"type": "object",
"properties": {
@@ -3388,6 +3497,56 @@
}
}
},
"handler.UptimeKuma.body": {
"type": "object",
"properties": {
"heartbeat": {
"type": "object",
"properties": {
"localDateTime": {
"type": "string"
},
"msg": {
"type": "string"
},
"status": {
"type": "integer"
},
"time": {
"type": "string"
},
"timezone": {
"type": "string"
},
"timezoneOffset": {
"type": "string"
}
}
},
"monitor": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"msg": {
"type": "string"
}
}
},
"handler.UptimeKuma.response": {
"type": "object",
"properties": {
"message_id": {
"type": "string"
}
}
},
"handler.pingResponse": {
"type": "object",
"properties": {
@@ -3574,9 +3733,6 @@
"message_id": {
"type": "string"
},
"owner_user_id": {
"type": "string"
},
"priority": {
"type": "integer"
},
@@ -3638,9 +3794,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 +3862,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"
},

View File

@@ -183,8 +183,6 @@ definitions:
permissions:
type: string
required:
- all_channels
- channels
- name
- permissions
type: object
@@ -378,29 +376,6 @@ definitions:
suppress_send:
type: boolean
type: object
handler.SendMessageCompat.response:
properties:
errhighlight:
type: integer
error:
$ref: '#/definitions/apierr.APIError'
is_pro:
type: boolean
message:
type: string
messagecount:
type: integer
quota:
type: integer
quota_max:
type: integer
scn_msg_id:
type: integer
success:
type: boolean
suppress_send:
type: boolean
type: object
handler.Sleep.response:
properties:
duration:
@@ -460,6 +435,38 @@ definitions:
user_id:
type: integer
type: object
handler.UptimeKuma.body:
properties:
heartbeat:
properties:
localDateTime:
type: string
msg:
type: string
status:
type: integer
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.UptimeKuma.response:
properties:
message_id:
type: string
type: object
handler.pingResponse:
properties:
info:
@@ -583,8 +590,6 @@ definitions:
type: string
message_id:
type: string
owner_user_id:
type: string
priority:
type: integer
sender_ip:
@@ -625,8 +630,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 +675,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:
@@ -1369,7 +1402,7 @@ paths:
summary: Delete a single message
tags:
- API-v2
patch:
get:
description: |-
The user must either own the message and request the resource with the READ or ADMIN Key
Or the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key
@@ -2153,13 +2186,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
@@ -2354,6 +2398,70 @@ paths:
summary: Update a subscription (e.g. confirm)
tags:
- API-v2
/external/v1/uptime-kuma:
post:
description: All parameter can be set via query-parameter or the json body.
Only UserID, UserKey and Title are required
parameters:
- in: query
name: channel
type: string
- in: query
name: channel_down
type: string
- in: query
name: channel_up
type: string
- example: P3TNH8mvv14fm
in: query
name: key
type: string
- in: query
name: priority
type: integer
- in: query
name: priority_down
type: integer
- in: query
name: priority_up
type: integer
- in: query
name: senderName
type: string
- example: "7725"
in: query
name: user_id
type: string
- description: ' '
in: body
name: post_body
schema:
$ref: '#/definitions/handler.UptimeKuma.body'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.UptimeKuma.response'
"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
/send:
post:
description: All parameter can be set via query-parameter or the json body.
@@ -2475,53 +2583,91 @@ paths:
description: All parameter can be set via query-parameter or form-data body.
Only UserID, UserKey and Title are required
parameters:
- in: query
- example: test
in: query
name: channel
type: string
- example: This is a message
in: query
name: content
type: string
- in: query
- example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id
type: string
- in: query
- enum:
- 0
- 1
- 2
example: 1
in: query
name: priority
type: integer
- in: query
- example: example-server
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp
type: number
- in: query
- example: Hello World
in: query
name: title
type: string
- in: query
- example: "7725"
in: query
name: user_id
type: integer
- in: query
name: user_key
type: string
- in: formData
- example: test
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content
type: string
- in: formData
- example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id
type: string
- in: formData
- enum:
- 0
- 1
- 2
example: 1
in: formData
name: priority
type: integer
- in: formData
- example: example-server
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp
type: number
- in: formData
- example: Hello World
in: formData
name: title
type: string
- in: formData
- example: "7725"
in: formData
name: user_id
type: integer
- in: formData
name: user_key
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.SendMessageCompat.response'
$ref: '#/definitions/handler.SendMessage.response'
"400":
description: Bad Request
schema:

View File

@@ -687,40 +687,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 +818,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{})
}

View File

@@ -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"])
}

View File

@@ -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)
}
}

View File

@@ -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{

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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"]))
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -1,3 +1,8 @@
package util
type FormData map[string]string
type RawJSON struct {
ContentType string
Body string
}

View File

@@ -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)
if len(respBodyBin) > 100_000 {
TPrintln("[[RESPONSE TOO LONG]]")
} else {
TPrintln(langext.TryPrettyPrintJson(string(respBodyBin)))
}
TryPrintTraceObj("---------------- -------- ----------------", respBodyBin, "")
TPrintln("---------------- -------- ----------------")
TPrintln("")

View File

@@ -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
}

View File

@@ -14,13 +14,10 @@
<span class="c1"># or scn_send &quot;@${channel} &quot;${title}&quot; ${content}&quot;</span>
<span class="c1"># or scn_send &quot;@${channel} &quot;${title}&quot; ${content}&quot; &quot;${priority:0|1|2}&quot;</span>
<span class="c1">#</span>
<span class="c1"># content can be of format &quot;--scnsend-read-body-from-file={path}&quot; 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">&quot;999&quot;</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">&quot;??&quot;</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>&gt;<span class="p">&amp;</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">&quot;\x1B[31m</span><span class="nv">$1</span><span class="s2">\x1B[0m&quot;</span><span class="p">;</span><span class="w"> </span><span class="k">else</span><span class="w"> </span>&gt;<span class="p">&amp;</span><span class="m">2</span><span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$1</span><span class="s2">&quot;</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">&quot;\x1B[32m</span><span class="nv">$1</span><span class="s2">\x1B[0m&quot;</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">&quot;</span><span class="nv">$1</span><span class="s2">&quot;</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 &#39;SCN_UID&#39; and &#39;SCN_KEY&#39; from conf file</span>
<span class="c1"># </span>
<span class="c1"># shellcheck source=/dev/null</span>
.<span class="w"> </span><span class="s2">&quot;/etc/scn.conf&quot;</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">&quot;</span><span class="si">${</span><span class="nv">SCN_UID</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">{</span><span class="w"> </span>rederr<span class="w"> </span><span class="s2">&quot;Missing config value &#39;SCN_UID&#39; in /etc/scn.conf&quot;</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">&quot;</span><span class="si">${</span><span class="nv">SCN_KEY</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">{</span><span class="w"> </span>rederr<span class="w"> </span><span class="s2">&quot;Missing config value &#39;SCN_KEY&#39; in /etc/scn.conf&quot;</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">&quot;</span><span class="nv">$@</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">)</span>
<span class="nv">title</span><span class="o">=</span><span class="s2">&quot;&quot;</span>
<span class="nv">content</span><span class="o">=</span><span class="s2">&quot;&quot;</span>
<span class="nv">channel</span><span class="o">=</span><span class="s2">&quot;&quot;</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">&quot;&quot;</span>
<span class="nv">usr_msg_id</span><span class="o">=</span><span class="s2">&quot;</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">&quot;</span>
<span class="nv">sendtime</span><span class="o">=</span><span class="s2">&quot;</span><span class="k">$(</span>date<span class="w"> </span>+%s<span class="k">)</span><span class="s2">&quot;</span>
<span class="nv">sender</span><span class="o">=</span><span class="s2">&quot;</span><span class="k">$(</span>hostname<span class="k">)</span><span class="s2">&quot;</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">&amp;</span>&gt;<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">&quot;</span><span class="k">$(</span><span class="w"> </span>srvname<span class="w"> </span><span class="k">)</span><span class="s2">&quot;</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">&quot;</span><span class="si">${</span><span class="nv">args</span><span class="p">[0]</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;--&quot;</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">&quot;</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">&quot;</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">&quot;[ERROR]: no title supplied via parameter&quot;</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">&quot;</span><span class="nv">$content</span><span class="s2">&quot;</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">&quot;</span><span class="k">$(</span><span class="w"> </span>awk<span class="w"> </span><span class="s1">&#39;{ print substr($0, 31) }&#39;</span><span class="w"> </span><span class="o">&lt;&lt;&lt;</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$content</span><span class="s2">&quot;</span><span class="w"> </span><span class="k">)</span><span class="s2">&quot;</span>
<span class="w"> </span><span class="nv">content</span><span class="o">=</span><span class="s2">&quot;</span><span class="k">$(</span><span class="w"> </span>cat<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$path</span><span class="s2">&quot;</span><span class="w"> </span><span class="k">)</span><span class="s2">&quot;</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">&quot;--data&quot;</span><span class="w"> </span><span class="s2">&quot;user_id=</span><span class="si">${</span><span class="nv">SCN_UID</span><span class="si">}</span><span class="s2">&quot;</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">&quot;--data&quot;</span><span class="w"> </span><span class="s2">&quot;key=</span><span class="si">${</span><span class="nv">SCN_KEY</span><span class="si">}</span><span class="s2">&quot;</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">&quot;--data&quot;</span><span class="w"> </span><span class="s2">&quot;title=</span><span class="nv">$title</span><span class="s2">&quot;</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">&quot;--data&quot;</span><span class="w"> </span><span class="s2">&quot;timestamp=</span><span class="nv">$sendtime</span><span class="s2">&quot;</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">&quot;--data&quot;</span><span class="w"> </span><span class="s2">&quot;msg_id=</span><span class="nv">$usr_msg_id</span><span class="s2">&quot;</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">&quot;--data-urlencode&quot;</span><span class="w"> </span><span class="s2">&quot;user_id=</span><span class="si">${</span><span class="nv">SCN_UID</span><span class="si">}</span><span class="s2">&quot;</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">&quot;--data-urlencode&quot;</span><span class="w"> </span><span class="s2">&quot;key=</span><span class="si">${</span><span class="nv">SCN_KEY</span><span class="si">}</span><span class="s2">&quot;</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">&quot;--data-urlencode&quot;</span><span class="w"> </span><span class="s2">&quot;title=</span><span class="nv">$title</span><span class="s2">&quot;</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">&quot;--data-urlencode&quot;</span><span class="w"> </span><span class="s2">&quot;timestamp=</span><span class="nv">$sendtime</span><span class="s2">&quot;</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">&quot;--data-urlencode&quot;</span><span class="w"> </span><span class="s2">&quot;msg_id=</span><span class="nv">$usr_msg_id</span><span class="s2">&quot;</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">&quot;</span><span class="nv">$content</span><span class="s2">&quot;</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">&quot;--data&quot;</span><span class="w"> </span><span class="s2">&quot;content=</span><span class="nv">$content</span><span class="s2">&quot;</span><span class="o">)</span>
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">&quot;--data-urlencode&quot;</span><span class="w"> </span><span class="s2">&quot;content=</span><span class="nv">$content</span><span class="s2">&quot;</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">&quot;</span><span class="nv">$priority</span><span class="s2">&quot;</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">&quot;--data&quot;</span><span class="w"> </span><span class="s2">&quot;priority=</span><span class="nv">$priority</span><span class="s2">&quot;</span><span class="o">)</span>
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">&quot;--data-urlencode&quot;</span><span class="w"> </span><span class="s2">&quot;priority=</span><span class="nv">$priority</span><span class="s2">&quot;</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">&quot;</span><span class="nv">$channel</span><span class="s2">&quot;</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">&quot;--data&quot;</span><span class="w"> </span><span class="s2">&quot;channel=</span><span class="nv">$channel</span><span class="s2">&quot;</span><span class="o">)</span>
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">&quot;--data-urlencode&quot;</span><span class="w"> </span><span class="s2">&quot;channel=</span><span class="nv">$channel</span><span class="s2">&quot;</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">&quot;</span><span class="nv">$sender</span><span class="s2">&quot;</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">&quot;--data&quot;</span><span class="w"> </span><span class="s2">&quot;sender_name=</span><span class="nv">$sender</span><span class="s2">&quot;</span><span class="o">)</span>
<span class="w"> </span><span class="nv">curlparams</span><span class="o">+=(</span><span class="s2">&quot;--data-urlencode&quot;</span><span class="w"> </span><span class="s2">&quot;sender_name=</span><span class="nv">$sender</span><span class="s2">&quot;</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>

184
web/.gitignore vendored
View File

@@ -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

View File

@@ -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">&#169; blackforestbytes</a>
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schw&ouml;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>

2
web/api/.gitignore vendored
View File

@@ -1,2 +0,0 @@
config.php
.verify_accesstoken

View File

@@ -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',
],
];

View File

@@ -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'
]);

View File

@@ -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'
]);

View File

@@ -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.

View File

@@ -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);
}
//#################################################################################################################

View File

@@ -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'
]);

View File

@@ -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,
]);

View File

@@ -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`)
);

View File

@@ -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'
]);
}

View File

@@ -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'
]);
}

View File

@@ -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">&#169; blackforestbytes</a>
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schw&ouml;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

Some files were not shown because too many files have changed in this diff Show More