40 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
1bb37eec30 TODO's 2023-06-18 14:28:58 +02:00
59511b2345 Fix bug in migration script 2023-06-18 14:16:37 +02:00
5b7bc02c61 Fix validation in web form 2023-06-18 13:25:00 +02:00
b329f537e7 Fix message_sent html 2023-06-18 13:19:51 +02:00
5879e81759 Enable RequestLog on dev/stag/prod 2023-06-18 13:11:48 +02:00
f4e88bef77 Fix NPE in compat-ack 2023-06-18 13:09:36 +02:00
b3ec45309c Insert exclam on compat clients if message uses old channel syntax 2023-06-18 11:59:26 +02:00
2fbc892898 various fixes in scn_send script 2023-06-18 04:45:28 +02:00
c46190c3fc Support x-www-form-urlencoded form-data 2023-06-18 03:46:01 +02:00
860e540de1 Better CreateKey API (make all_channels and channels optional) 2023-06-18 02:54:41 +02:00
8cde286cac Fix failing tests 2023-06-18 02:36:44 +02:00
90830fe384 Fix empty string in channels field in GetKeys route 2023-06-18 02:34:04 +02:00
686f89f75d change URL to simplecloudnotifier.de 2023-06-18 02:22:29 +02:00
4210af5680 Properly implement compat unack_count 2023-06-18 02:09:05 +02:00
aefc368cfd Send compat-msgid to compat clients (BF old android client) 2023-06-18 01:55:58 +02:00
67218d8045 Added a few logs 2023-06-18 01:36:34 +02:00
c05deb3a41 allow \n in private-key envs 2023-06-18 01:29:13 +02:00
43d0107fb5 Update goext (fix bool env parsing) 2023-06-18 01:18:33 +02:00
ece7612f9d more migration fixes 2023-06-18 00:49:29 +02:00
a9809d90cb fix exception in js-send (logic.js) 2023-06-18 00:25:10 +02:00
bbc9a79996 Fix bug in migration script 2023-06-18 00:24:53 +02:00
b71f1885ec Fix wrongly named env variables 2023-06-17 23:40:46 +02:00
885aad2047 Update migrationscript 2023-06-17 23:23:54 +02:00
123 changed files with 18535 additions and 3619 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

@@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# Wrapper around SCN ( https://scn.blackforestbytes.com/ ) # Wrapper around SCN ( https://simplecloudnotifier.de/ )
# ======================================================== # ======================================================
# #
# ./scn_send [@channel] title [content] [priority] # ./scn_send [@channel] title [content] [priority]
# #
@@ -14,13 +14,10 @@
# or scn_send "@${channel} "${title}" ${content}" # or scn_send "@${channel} "${title}" ${content}"
# or scn_send "@${channel} "${title}" ${content}" "${priority:0|1|2}" # 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() { 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 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; } 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=( "$@" ) args=( "$@" )
title=$1 title=""
content="" content=""
channel="" channel=""
priority=1 priority=""
usr_msg_id="$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)" usr_msg_id="$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)"
sendtime="$(date +%s)" sendtime="$(date +%s)"
sender="$(hostname)" 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 if [ ${#args[@]} -lt 1 ]; then
rederr "[ERROR]: no title supplied via parameter" rederr "[ERROR]: no title supplied via parameter"
usage usage
@@ -51,9 +72,9 @@ if [ ${#args[@]} -lt 1 ]; then
fi fi
if [[ "${args[0]}" =~ ^@.* ]]; then if [[ "${args[0]}" =~ ^@.* ]]; then
channel="${args[0]}" channel="${args[0]}"
unset "args[0]" args=("${args[@]:1}")
channel="${channel:1}" channel="${channel:1}"
fi fi
if [ ${#args[@]} -lt 1 ]; then if [ ${#args[@]} -lt 1 ]; then
@@ -63,24 +84,54 @@ if [ ${#args[@]} -lt 1 ]; then
fi fi
title="${args[0]}" title="${args[0]}"
args=("${args[@]:1}")
content="" content=""
if [ ${#args[@]} -gt 1 ]; then if [ ${#args[@]} -gt 0 ]; then
content="${args[0]}" content="${args[0]}"
unset "args[0]" args=("${args[@]:1}")
fi fi
if [ ${#args[@]} -gt 1 ]; then if [ ${#args[@]} -gt 0 ]; then
priority="${args[0]}" priority="${args[0]}"
unset "args[0]" args=("${args[@]:1}")
fi fi
if [ ${#args[@]} -gt 1 ]; then if [ ${#args[@]} -gt 0 ]; then
rederr "Too many arguments to scn_send" rederr "Too many arguments to scn_send"
usage usage
exit 1 exit 1
fi fi
if [[ "$content" == --scnsend-read-body-from-file=* ]]; then
path="$( awk '{ print substr($0, 31) }' <<< "$content" )"
content="$( cat "$path" )"
fi
curlparams=()
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-urlencode" "content=$content")
fi
if [[ -n "$priority" ]]; then
curlparams+=("--data-urlencode" "priority=$priority")
fi
if [[ -n "$channel" ]]; then
curlparams+=("--data-urlencode" "channel=$channel")
fi
if [[ -n "$sender" ]]; then
curlparams+=("--data-urlencode" "sender_name=$sender")
fi
while true ; do while true ; do
@@ -89,16 +140,8 @@ while true ; do
curlresp=$(curl --silent \ curlresp=$(curl --silent \
--output "${outf}" \ --output "${outf}" \
--write-out "%{http_code}" \ --write-out "%{http_code}" \
--data "user_id=$user_id" \ "${curlparams[@]}" \
--data "key=$user_key" \ "https://simplecloudnotifier.de/" )
--data "title=$title" \
--data "timestamp=$sendtime" \
--data "content=$content" \
--data "priority=$priority" \
--data "msg_id=$usr_msg_id" \
--data "channel=$channel" \
--data "sender_name=$sender" \
"https://scn.blackforestbytes.com/" )
curlout="$(cat "$outf")" curlout="$(cat "$outf")"
rm "$outf" rm "$outf"

10
scnserver/.gitignore vendored
View File

@@ -8,10 +8,20 @@ DOCKER_GIT_INFO
scn_export.dat scn_export.dat
scn_export.json scn_export.json
scn_export_*.dat
scn_export_*.json
simple_cloud_notifier-202306172202.sql
simple_cloud_notifier-*.sql
identifier.sqlite identifier.sqlite
.idea/dataSources.xml .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 && \ RUN apt-get update && \
apt-get install -y ca-certificates openssl make git tar coreutils && \ 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/* rm -rf /var/lib/apt/lists/*
COPY . /buildsrc COPY . /buildsrc
RUN cd /buildsrc && make build RUN cd /buildsrc && cp "scn_send.sh" "../scn_send.sh" && make build

View File

@@ -5,14 +5,22 @@ PORT=9090
NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD) NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD)
HASH=$(shell git rev-parse HEAD) HASH=$(shell git rev-parse HEAD)
.PHONY: test swagger pygmentize docker migrate dgi pygmentize lint .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 mkdir -p _build
rm -f ./_build/scn_backend 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 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 run: build
mkdir -p .run-data mkdir -p .run-data
_build/scn_backend _build/scn_backend
@@ -29,7 +37,8 @@ dgi:
echo -n "COMMITTIME=" >> DOCKER_GIT_INFO ; git log -1 --format=%cd --date=iso >> DOCKER_GIT_INFO echo -n "COMMITTIME=" >> DOCKER_GIT_INFO ; git log -1 --format=%cd --date=iso >> DOCKER_GIT_INFO
echo -n "REMOTE=" >> DOCKER_GIT_INFO ; git config --get remote.origin.url >> DOCKER_GIT_INFO echo -n "REMOTE=" >> DOCKER_GIT_INFO ; git config --get remote.origin.url >> DOCKER_GIT_INFO
build-docker: dgi docker: dgi
cp ../scn_send.sh .
docker build \ docker build \
-t "$(DOCKER_NAME):$(HASH)" \ -t "$(DOCKER_NAME):$(HASH)" \
-t "$(DOCKER_NAME):$(NAMESPACE)-latest" \ -t "$(DOCKER_NAME):$(NAMESPACE)-latest" \
@@ -38,17 +47,21 @@ build-docker: dgi
-t "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest" \ -t "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest" \
-t "$(DOCKER_REPO)/$(DOCKER_NAME):latest" \ -t "$(DOCKER_REPO)/$(DOCKER_NAME):latest" \
. .
[ -f "scn_send.sh" ] && rm scn_send.sh
swagger: swagger-setup:
which swag || go install github.com/swaggo/swag/cmd/swag@v1.8.12 mkdir -p ".swaggobin"
swag init -generalInfo api/router.go --propertyStrategy snakecase --output ./swagger/ --outputTypes "json,yaml" [ -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 pygmentize: website/scn_send.html
website/scn_send.html: website/scn_send.sh.txt website/scn_send.html: ../scn_send.sh
_pygments/pygmentizew -l bash -f html "$(shell pwd)/website/scn_send.sh.txt" > "$(shell pwd)/website/scn_send.html" _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 monokai -f html > "$(shell pwd)/website/css/pygmnetize-dark.css"
_pygments/pygmentizew -S borland -f html > "$(shell pwd)/website/css/pygmnetize-light.css" _pygments/pygmentizew -S borland -f html > "$(shell pwd)/website/css/pygmnetize-light.css"
run-docker-local: docker run-docker-local: docker
mkdir -p .run-data mkdir -p .run-data
@@ -67,7 +80,7 @@ inspect-docker: docker
$(DOCKER_NAME):latest \ $(DOCKER_NAME):latest \
bash bash
push-docker: docker push-docker:
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(HASH)" docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(HASH)"
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest" docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest"
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):latest" docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):latest"
@@ -75,13 +88,14 @@ push-docker: docker
clean: clean:
rm -rf _build/* rm -rf _build/*
rm -rf .run-data/* rm -rf .run-data/*
rm -rf _pygments/env
git clean -fdx git clean -fdx
go clean ! which go 2>&1 >> /dev/null || go clean
go clean -testcache ! which go 2>&1 >> /dev/null || go clean -testcache
fmt: fmt: swagger-setup
go fmt ./... go fmt ./...
swag fmt ".swaggobin/swag_$(SWAGGO_VERSION)" fmt
test: test:
which gotestsum || go install gotest.tools/gotestsum@latest which gotestsum || go install gotest.tools/gotestsum@latest

View File

@@ -4,20 +4,18 @@
======== ========
#### BEFORE RELEASE #### DO DO DO
- migrate old data
- in my script: use `srvname` for sendername
- switch send script everywhere (we can use the new server, but we need to send correct channels)
- app-store link in HTML - app-store link in HTML
- deploy
- ios purchase verification - ios purchase verification
- (!) use goext.ginWrapper
- (!) use goext.exerr
- use bfcodegen (enums+id)
#### UNSURE #### UNSURE
- (?) default-priority for channels - (?) default-priority for channels
@@ -30,6 +28,8 @@
- (?) add querylog (similar to requestlog/errorlog) - only for main-db - (?) add querylog (similar to requestlog/errorlog) - only for main-db
- (?) specify 'type' of message (debug, info, warn, error, fatal) -> distinct from priority
#### LATER #### LATER
- do i need bool2db()? it seems to work for keytokens without them? - do i need bool2db()? it seems to work for keytokens without them?
@@ -51,10 +51,6 @@
- route to re-check all pro-token (for me) - 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) - endpoint to list all servernames of user (distinct select)
- weblogin, webapp, ... - weblogin, webapp, ...
@@ -68,6 +64,10 @@
- use job superclass (copy from isi/bnet/?), reduce duplicate code - use job superclass (copy from isi/bnet/?), reduce duplicate code
- admin panel (especially errors and requests)
- cli app (?)
#### FUTURE #### FUTURE
- Remove compat, especially do not create compat id for every new message... - 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

@@ -68,6 +68,12 @@ func Wrap(rlacc RequestLogAcceptor, fn WHandlerFunc) gin.HandlerFunc {
if scn.Conf.ReqLogEnabled { if scn.Conf.ReqLogEnabled {
rlacc.InsertRequestLog(createRequestLog(g, t0, ctr, wrap, nil)) rlacc.InsertRequestLog(createRequestLog(g, t0, ctr, wrap, nil))
} }
statuscode := wrap.Statuscode()
if statuscode/100 != 2 {
log.Warn().Str("url", g.Request.Method+"::"+g.Request.URL.String()).Msg(fmt.Sprintf("Request failed with statuscode %d", statuscode))
}
wrap.Write(g) wrap.Write(g)
} }

View File

@@ -6,6 +6,7 @@ import (
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken" ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "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) 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) return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
if err != nil { if err != nil {
@@ -206,7 +207,7 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
} }
user, err := h.database.GetUser(ctx, u.UserID) 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) return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
} }
if err != 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) _, 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) return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
if err != nil { if err != nil {
@@ -306,7 +307,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
} }
user, err := h.database.GetUser(ctx, u.UserID) 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) return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
} }
if err != nil { if err != nil {
@@ -348,8 +349,8 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
descName = langext.Ptr(strings.TrimSpace(*b.DescriptionName)) descName = langext.Ptr(strings.TrimSpace(*b.DescriptionName))
} }
if descName != nil && len(*descName) > user.MaxChannelDescriptionNameLength() { 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.MaxChannelNameLength()), nil) 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) 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) pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize)
channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID, false) 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) return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
if err != nil { if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "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) 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) return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
} }
if err != nil { 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) 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) return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
} }
if err != nil { 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) 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) return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
} }
if err != nil { if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
@@ -45,12 +46,12 @@ func (h APIHandler) ListUserKeys(g *gin.Context) ginresp.HTTPResponse {
return *permResp return *permResp
} }
clients, err := h.database.ListKeyTokens(ctx, u.UserID) toks, err := h.database.ListKeyTokens(ctx, u.UserID)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query keys", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query keys", err)
} }
res := langext.ArrMap(clients, func(v models.KeyToken) models.KeyTokenJSON { return v.JSON() }) res := langext.ArrMap(toks, func(v models.KeyToken) models.KeyTokenJSON { return v.JSON() })
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Keys: res})) return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Keys: res}))
} }
@@ -90,7 +91,7 @@ func (h APIHandler) GetUserKey(g *gin.Context) ginresp.HTTPResponse {
} }
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) 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) return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
} }
if err != nil { 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) 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) return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
} }
if err != nil { if err != nil {
@@ -221,9 +222,9 @@ func (h APIHandler) CreateUserKey(g *gin.Context) ginresp.HTTPResponse {
} }
type body struct { type body struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
AllChannels *bool `json:"all_channels" binding:"required"` Permissions string `json:"permissions" binding:"required"`
Channels *[]models.ChannelID `json:"channels" binding:"required"` AllChannels *bool `json:"all_channels"`
Permissions *string `json:"permissions" binding:"required"` Channels *[]models.ChannelID `json:"channels"`
} }
var u uri var u uri
@@ -234,7 +235,18 @@ func (h APIHandler) CreateUserKey(g *gin.Context) ginresp.HTTPResponse {
} }
defer ctx.Cancel() defer ctx.Cancel()
for _, c := range *b.Channels { channels := langext.Coalesce(b.Channels, make([]models.ChannelID, 0))
var allChan bool
if b.AllChannels == nil && b.Channels != nil {
allChan = false
} else if b.AllChannels == nil && b.Channels == nil {
allChan = true
} else {
allChan = *b.AllChannels
}
for _, c := range channels {
if err := c.Valid(); err != nil { if err := c.Valid(); err != nil {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Invalid ChannelID", err) return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Invalid ChannelID", err)
} }
@@ -246,9 +258,9 @@ func (h APIHandler) CreateUserKey(g *gin.Context) ginresp.HTTPResponse {
token := h.app.GenerateRandomAuthKey() token := h.app.GenerateRandomAuthKey()
perms := models.ParseTokenPermissionList(*b.Permissions) perms := models.ParseTokenPermissionList(b.Permissions)
keytok, err := h.database.CreateKeyToken(ctx, b.Name, *ctx.GetPermissionUserID(), *b.AllChannels, *b.Channels, perms, token) keytok, err := h.database.CreateKeyToken(ctx, b.Name, *ctx.GetPermissionUserID(), allChan, channels, perms, token)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create keytoken in db", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create keytoken in db", err)
} }
@@ -291,7 +303,7 @@ func (h APIHandler) DeleteUserKey(g *gin.Context) ginresp.HTTPResponse {
} }
client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) 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) return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
} }
if err != nil { if err != nil {

View File

@@ -1,17 +1,19 @@
package handler package handler
import ( import (
"database/sql"
"errors"
"net/http"
"strings"
"time"
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken" ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "gogs.mikescher.com/BlackForestBytes/goext/mathext"
"net/http"
"strings"
"time"
) )
// ListMessages swaggerdoc // ListMessages swaggerdoc
@@ -173,7 +175,7 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError "message not found" // @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @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 { func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
MessageID models.MessageID `uri:"mid" binding:"entityid"` 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) 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) return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
} }
if err != nil { 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) 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) return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
} }
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) 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) 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/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
@@ -14,13 +15,25 @@ import (
// ListUserSubscriptions swaggerdoc // ListUserSubscriptions swaggerdoc
// //
// @Summary List all subscriptions of a user (incoming/owned) // @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 The possible values for 'direction' are:
// @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber // @Description - "outgoing" Subscriptions with the user as subscriber (= subscriptions he can use to read channels)
// @Description - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber // @Description - "incoming" Subscriptions to channels of this user (= incoming subscriptions and subscription requests)
// @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests) // @Description - "both" Combines "outgoing" and "incoming" (default)
// @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user // @Description
// @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests) // @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 // @ID api-user-subscriptions-list
// @Tags API-v2 // @Tags API-v2
@@ -39,7 +52,11 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
type query struct { 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 { type response struct {
Subscriptions []models.SubscriptionJSON `json:"subscriptions"` Subscriptions []models.SubscriptionJSON `json:"subscriptions"`
@@ -57,57 +74,56 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
return *permResp return *permResp
} }
sel := strings.ToLower(langext.Coalesce(q.Selector, "outgoing_all")) filter := models.SubscriptionFilter{}
filter.AnyUserID = langext.Ptr(u.UserID)
var res []models.Subscription if q.Direction != nil {
var err error if strings.EqualFold(*q.Direction, "incoming") {
filter.ChannelOwnerUserID = langext.Ptr([]models.UserID{u.UserID})
if sel == "outgoing_all" { } else if strings.EqualFold(*q.Direction, "outgoing") {
filter.SubscriberUserID = langext.Ptr([]models.UserID{u.UserID})
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil) } else if strings.EqualFold(*q.Direction, "both") {
if err != nil { // both
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) } else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'direction'", nil)
} }
}
} else if sel == "outgoing_confirmed" { if q.Confirmation != nil {
if strings.EqualFold(*q.Confirmation, "confirmed") {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true)) filter.Confirmed = langext.PTrue
if err != nil { } else if strings.EqualFold(*q.Confirmation, "unconfirmed") {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) filter.Confirmed = langext.PFalse
} else if strings.EqualFold(*q.Confirmation, "all") {
// both
} else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'confirmation'", nil)
} }
}
} else if sel == "outgoing_unconfirmed" { if q.External != nil {
if strings.EqualFold(*q.External, "true") {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false)) filter.SubscriberIsChannelOwner = langext.PFalse
if err != nil { } else if strings.EqualFold(*q.External, "false") {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) filter.SubscriberIsChannelOwner = langext.PTrue
} else if strings.EqualFold(*q.External, "all") {
// both
} else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'external'", nil)
} }
}
} else if sel == "incoming_all" { if q.SubscriberUserID != nil {
filter.SubscriberUserID2 = langext.Ptr([]models.UserID{*q.SubscriberUserID})
}
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, nil) if q.ChannelOwnerUserID != nil {
if err != nil { filter.ChannelOwnerUserID2 = langext.Ptr([]models.UserID{*q.ChannelOwnerUserID})
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) }
}
} else if sel == "incoming_confirmed" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_unconfirmed" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(false))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else {
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
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() }) 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) _, 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) return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) 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 { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) 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) 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) return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
} }
if err != nil { if err != nil {
@@ -250,7 +266,7 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
} }
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) 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) return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
} }
if err != nil { if err != nil {
@@ -414,7 +430,7 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
userid := *ctx.GetPermissionUserID() userid := *ctx.GetPermissionUserID()
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) 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) return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
} }
if err != nil { if err != nil {

View File

@@ -5,7 +5,10 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
) )
@@ -113,6 +116,8 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create read-key in db", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create read-key in db", err)
} }
log.Info().Msg(fmt.Sprintf("Sucessfully created new user %s (client: %v)", userobj.UserID, b.NoClient))
if b.NoClient { if b.NoClient {
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients(make([]models.Client, 0), adminKey, sendKey, readKey))) return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients(make([]models.Client, 0), adminKey, sendKey, readKey)))
} else { } else {
@@ -163,7 +168,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
} }
user, err := h.database.GetUser(ctx, u.UserID) 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) return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
} }
if err != nil { if err != nil {

View File

@@ -3,6 +3,7 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"bytes" "bytes"
"context" "context"
@@ -127,7 +128,9 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
return ginresp.InternalError(errors.New("sqlite version too low")) 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 { if err != nil {
return ginresp.InternalError(err) return ginresp.InternalError(err)
} }
@@ -137,12 +140,12 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
uuidKey, _ := langext.NewHexUUID() uuidKey, _ := langext.NewHexUUID()
uuidWrite, _ := langext.NewHexUUID() uuidWrite, _ := langext.NewHexUUID()
err = subdb.WriteMetaString(ctx, uuidKey, uuidWrite) err = subdb.WriteMetaString(tctx, uuidKey, uuidWrite)
if err != nil { if err != nil {
return ginresp.InternalError(err) return ginresp.InternalError(err)
} }
uuidRead, err := subdb.ReadMetaString(ctx, uuidKey) uuidRead, err := subdb.ReadMetaString(tctx, uuidKey)
if err != nil { if err != nil {
return ginresp.InternalError(err) 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")) return ginresp.InternalError(errors.New("writing into DB was not consistent"))
} }
err = subdb.DeleteMeta(ctx, uuidKey) err = subdb.DeleteMeta(tctx, uuidKey)
if err != nil { if err != nil {
return ginresp.InternalError(err) return ginresp.InternalError(err)
} }

View File

@@ -9,6 +9,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/dataext" "gogs.mikescher.com/BlackForestBytes/goext/dataext"
@@ -28,7 +29,7 @@ func NewCompatHandler(app *logic.Application) CompatHandler {
} }
} }
// SendMessageCompat swaggerdoc // SendMessage swaggerdoc
// //
// @Deprecated // @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 // @Description All parameter can be set via query-parameter or form-data body. Only UserID, UserKey and Title are required
// @Tags External // @Tags External
// //
// @Param query_data query handler.SendMessageCompat.combined false " " // @Param query_data query handler.SendMessage.combined false " "
// @Param form_data formData handler.SendMessageCompat.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 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError // @Failure 401 {object} ginresp.apiError
// @Failure 403 {object} ginresp.apiError // @Failure 403 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /send.php [POST] // @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 { type combined struct {
UserID *int64 `json:"user_id" form:"user_id"` UserID *int64 `json:"user_id" form:"user_id"`
UserKey *string `json:"user_key" form:"user_key"` 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) 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 { if errResp != nil {
return *errResp return *errResp
} else { } else {
@@ -258,7 +259,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
QuotaMax int `json:"quota_max"` QuotaMax int `json:"quota_max"`
IsPro int `json:"is_pro"` IsPro int `json:"is_pro"`
FCMSet bool `json:"fcm_token_set"` FCMSet bool `json:"fcm_token_set"`
UnackCount int `json:"unack_count"` UnackCount int64 `json:"unack_count"`
} }
var datq query var datq query
@@ -287,7 +288,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
} }
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) 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") return ginresp.CompatAPIError(201, "User not found")
} }
if err != nil { if err != nil {
@@ -295,7 +296,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
} }
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed") return ginresp.CompatAPIError(204, "Authentification failed")
} }
if err != nil { if err != nil {
@@ -310,6 +311,16 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query clients") return ginresp.CompatAPIError(0, "Failed to query clients")
} }
filter := models.MessageFilter{
Sender: langext.Ptr([]models.UserID{user.UserID}),
CompatAcknowledged: langext.Ptr(false),
}
unackCount, err := h.database.CountMessages(ctx, filter)
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query user")
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{
Success: true, Success: true,
Message: "ok", Message: "ok",
@@ -319,7 +330,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
QuotaMax: user.QuotaPerDay(), QuotaMax: user.QuotaPerDay(),
IsPro: langext.Conditional(user.IsPro, 1, 0), IsPro: langext.Conditional(user.IsPro, 1, 0),
FCMSet: len(clients) > 0, FCMSet: len(clients) > 0,
UnackCount: 0, UnackCount: unackCount,
})) }))
} }
@@ -381,11 +392,11 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid<old>", err) return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid<old>", err)
} }
if useridCompNew == nil { if useridCompNew == nil {
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, fmt.Sprintf("User %d not found (compat)", *data.UserID), nil)
} }
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) 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") return ginresp.CompatAPIError(201, "User not found")
} }
if err != nil { if err != nil {
@@ -393,7 +404,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
} }
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed") return ginresp.CompatAPIError(204, "Authentification failed")
} }
if err != nil { if err != nil {
@@ -407,8 +418,8 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
if err != nil { if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query messageid<old>", err) return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query messageid<old>", err)
} }
if useridCompNew == nil { if messageIdComp == nil {
return ginresp.SendAPIError(g, 400, apierr.MESSAGE_NOT_FOUND, hl.USER_ID, "Message not found (compat)", nil) return ginresp.SendAPIError(g, 400, apierr.MESSAGE_NOT_FOUND, hl.NONE, fmt.Sprintf("Message %d not found (compat)", *data.MessageID), nil)
} }
ackBefore, err := h.database.GetAck(ctx, models.MessageID(*messageIdComp)) ackBefore, err := h.database.GetAck(ctx, models.MessageID(*messageIdComp))
@@ -487,7 +498,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
} }
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) 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") return ginresp.CompatAPIError(201, "User not found")
} }
if err != nil { if err != nil {
@@ -495,7 +506,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
} }
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed") return ginresp.CompatAPIError(204, "Authentification failed")
} }
if err != nil { if err != nil {
@@ -506,7 +517,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
} }
filter := models.MessageFilter{ filter := models.MessageFilter{
Owner: langext.Ptr([]models.UserID{user.UserID}), Sender: langext.Ptr([]models.UserID{user.UserID}),
CompatAcknowledged: langext.Ptr(false), CompatAcknowledged: langext.Ptr(false),
} }
@@ -518,13 +529,13 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
compMsgs := make([]models.CompatMessage, 0, len(msgs)) compMsgs := make([]models.CompatMessage, 0, len(msgs))
for _, v := range 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 { if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create messageid<old>", err) return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create messageid<old>", err)
} }
compMsgs = append(compMsgs, models.CompatMessage{ compMsgs = append(compMsgs, models.CompatMessage{
Title: compatizeMessageTitle(ctx, h.app, v), Title: h.app.CompatizeMessageTitle(ctx, v),
Body: v.Content, Body: v.Content,
Priority: v.Priority, Priority: v.Priority,
Timestamp: v.Timestamp().Unix(), Timestamp: v.Timestamp().Unix(),
@@ -604,7 +615,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
} }
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) 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") return ginresp.CompatAPIError(201, "User not found")
} }
if err != nil { if err != nil {
@@ -612,7 +623,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
} }
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed") return ginresp.CompatAPIError(204, "Authentification failed")
} }
if err != nil { if err != nil {
@@ -734,7 +745,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
} }
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) 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") return ginresp.CompatAPIError(201, "User not found")
} }
if err != nil { if err != nil {
@@ -742,7 +753,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
} }
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed") return ginresp.CompatAPIError(204, "Authentification failed")
} }
if err != nil { if err != nil {
@@ -761,7 +772,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
} }
msg, err := h.database.GetMessage(ctx, models.MessageID(*messageCompNew), false) 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") return ginresp.CompatAPIError(301, "Message not found")
} }
if err != nil { if err != nil {
@@ -772,7 +783,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
Success: true, Success: true,
Message: "ok", Message: "ok",
Data: models.CompatMessage{ Data: models.CompatMessage{
Title: compatizeMessageTitle(ctx, h.app, msg), Title: h.app.CompatizeMessageTitle(ctx, msg),
Body: msg.Content, Body: msg.Content,
Trimmed: langext.Ptr(false), Trimmed: langext.Ptr(false),
Priority: msg.Priority, Priority: msg.Priority,
@@ -853,7 +864,7 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
} }
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) 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") return ginresp.CompatAPIError(201, "User not found")
} }
if err != nil { if err != nil {
@@ -861,7 +872,7 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
} }
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed") return ginresp.CompatAPIError(204, "Authentification failed")
} }
if err != nil { if err != nil {
@@ -919,16 +930,3 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
IsPro: user.IsPro, IsPro: user.IsPro,
})) }))
} }
func compatizeMessageTitle(ctx *logic.AppContext, app *logic.Application, msg models.Message) string {
if msg.ChannelInternalName == "main" {
return msg.Title
}
channel, err := app.Database.Primary.GetChannelByID(ctx, msg.ChannelID)
if err != nil {
return fmt.Sprintf("[%s] %s", "%SCN-ERR%", msg.Title)
}
return fmt.Sprintf("[%s] %s", channel.DisplayName, msg.Title)
}

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,21 +2,14 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary" primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/dataext" "gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"net/http" "net/http"
"strings"
"time"
) )
type SendMessageResponse struct { type SendMessageResponse struct {
@@ -94,7 +87,7 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
// query has highest prio, then form, then json // query has highest prio, then form, then json
data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q) 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 { if errResp != nil {
return *errResp return *errResp
} else { } else {
@@ -112,199 +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))
}
cid, err := h.database.CreateCompatID(ctx, "messageid", msg.MessageID.String())
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
}
subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query subscriptions", err))
}
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))
}
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
if isCompatClient {
titleOverride = langext.Ptr(compatizeMessageTitle(ctx, h.app, msg))
}
fcmDelivID, err := h.app.DeliverMessage(ctx, client, msg, titleOverride)
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: cid,
}, nil
}

View File

@@ -16,22 +16,24 @@ import (
type Router struct { type Router struct {
app *logic.Application app *logic.Application
commonHandler handler.CommonHandler commonHandler handler.CommonHandler
compatHandler handler.CompatHandler compatHandler handler.CompatHandler
websiteHandler handler.WebsiteHandler websiteHandler handler.WebsiteHandler
apiHandler handler.APIHandler apiHandler handler.APIHandler
messageHandler handler.MessageHandler messageHandler handler.MessageHandler
externalHandler handler.ExternalHandler
} }
func NewRouter(app *logic.Application) *Router { func NewRouter(app *logic.Application) *Router {
return &Router{ return &Router{
app: app, app: app,
commonHandler: handler.NewCommonHandler(app), commonHandler: handler.NewCommonHandler(app),
compatHandler: handler.NewCompatHandler(app), compatHandler: handler.NewCompatHandler(app),
websiteHandler: handler.NewWebsiteHandler(app), websiteHandler: handler.NewWebsiteHandler(app),
apiHandler: handler.NewAPIHandler(app), apiHandler: handler.NewAPIHandler(app),
messageHandler: handler.NewMessageHandler(app), messageHandler: handler.NewMessageHandler(app),
externalHandler: handler.NewExternalHandler(app),
} }
} }
@@ -40,7 +42,7 @@ func NewRouter(app *logic.Application) *Router {
// @title SimpleCloudNotifier API // @title SimpleCloudNotifier API
// @version 2.0 // @version 2.0
// @description API for SCN // @description API for SCN
// @host scn.blackforestbytes.com // @host simplecloudnotifier.de
// //
// @tag.name External // @tag.name External
// @tag.name API-v1 // @tag.name API-v1
@@ -122,7 +124,6 @@ func (r *Router) Init(e *gin.Engine) error {
apiv2 := e.Group("/api/v2/") apiv2 := e.Group("/api/v2/")
{ {
apiv2.POST("/users", r.Wrap(r.apiHandler.CreateUser)) apiv2.POST("/users", r.Wrap(r.apiHandler.CreateUser))
apiv2.GET("/users/:uid", r.Wrap(r.apiHandler.GetUser)) apiv2.GET("/users/:uid", r.Wrap(r.apiHandler.GetUser))
apiv2.PATCH("/users/:uid", r.Wrap(r.apiHandler.UpdateUser)) 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("/", r.Wrap(r.messageHandler.SendMessage))
sendAPI.POST("/send", 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) 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) h0, err := sq.HashSqliteSchema(ctx, schema.RequestsSchema1)
if err != nil { if err != nil {

View File

@@ -13,6 +13,7 @@ import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext" "gogs.mikescher.com/BlackForestBytes/goext/rext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"gogs.mikescher.com/BlackForestBytes/goext/termext"
"os" "os"
"regexp" "regexp"
"strings" "strings"
@@ -89,9 +90,10 @@ func main() {
panic(err) panic(err)
} }
scanner := bufio.NewScanner(os.Stdin)
connstr := os.Getenv("SQL_CONN_STR") connstr := os.Getenv("SQL_CONN_STR")
if connstr == "" { if connstr == "" {
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Enter DB URL [127.0.0.1:3306]: ") fmt.Print("Enter DB URL [127.0.0.1:3306]: ")
scanner.Scan() scanner.Scan()
@@ -103,21 +105,33 @@ func main() {
fmt.Print("Enter DB Username [root]: ") fmt.Print("Enter DB Username [root]: ")
scanner.Scan() scanner.Scan()
username := scanner.Text() username := scanner.Text()
if host == "" { if username == "" {
host = "root" username = "root"
} }
fmt.Print("Enter DB Password []: ") fmt.Print("Enter DB Password []: ")
scanner.Scan() scanner.Scan()
pass := scanner.Text() pass := scanner.Text()
if host == "" { if pass == "" {
host = "" pass = ""
} }
connstr = fmt.Sprintf("%s:%s@tcp(%s)", username, pass, host) connstr = fmt.Sprintf("%s:%s@tcp(%s)", username, pass, host)
} }
_dbold, err := sqlx.Open("mysql", connstr+"/simple_cloud_notifier?parseTime=true") olddbname := os.Getenv("SQL_CONN_DBNAME")
if olddbname == "" {
fmt.Print("Enter DB Name: ")
scanner.Scan()
olddbname = scanner.Text()
if olddbname == "" {
olddbname = "scn_final"
}
}
_dbold, err := sqlx.Open("mysql", connstr+"/"+olddbname+"?parseTime=true")
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -292,8 +306,8 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
_, err = dbnew.Exec(ctx, "INSERT INTO subscriptions (subscription_id, subscriber_user_id, channel_owner_user_id, channel_internal_name, channel_id, timestamp_created, confirmed) VALUES (:sid, :suid, :ouid, :cnam, :cid, :ts, :conf)", sq.PP{ _, err = dbnew.Exec(ctx, "INSERT INTO subscriptions (subscription_id, subscriber_user_id, channel_owner_user_id, channel_internal_name, channel_id, timestamp_created, confirmed) VALUES (:sid, :suid, :ouid, :cnam, :cid, :ts, :conf)", sq.PP{
"sid": models.NewSubscriptionID(), "sid": models.NewSubscriptionID(),
"suid": user.UserId, "suid": userid,
"ouid": user.UserId, "ouid": userid,
"cnam": "main", "cnam": "main",
"cid": mainChannelID, "cid": mainChannelID,
"ts": user.TimestampCreated.UnixMilli(), "ts": user.TimestampCreated.UnixMilli(),
@@ -340,7 +354,7 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
dispName := dummyApp.NormalizeChannelDisplayName(chanNameTitle) dispName := dummyApp.NormalizeChannelDisplayName(chanNameTitle)
intName := dummyApp.NormalizeChannelInternalName(chanNameTitle) intName := dummyApp.NormalizeChannelInternalName(chanNameTitle)
if v, ok := channelMap[intName]; ok { if v, ok := channelMap[strings.ToLower(intName)]; ok {
channelID = v channelID = v
channelInternalName = intName channelInternalName = intName
} else { } else {
@@ -363,8 +377,8 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
_, err = dbnew.Exec(ctx, "INSERT INTO subscriptions (subscription_id, subscriber_user_id, channel_owner_user_id, channel_internal_name, channel_id, timestamp_created, confirmed) VALUES (:sid, :suid, :ouid, :cnam, :cid, :ts, :conf)", sq.PP{ _, err = dbnew.Exec(ctx, "INSERT INTO subscriptions (subscription_id, subscriber_user_id, channel_owner_user_id, channel_internal_name, channel_id, timestamp_created, confirmed) VALUES (:sid, :suid, :ouid, :cnam, :cid, :ts, :conf)", sq.PP{
"sid": models.NewSubscriptionID(), "sid": models.NewSubscriptionID(),
"suid": user.UserId, "suid": userid,
"ouid": user.UserId, "ouid": userid,
"cnam": intName, "cnam": intName,
"cid": channelID, "cid": channelID,
"ts": oldmessage.TimestampReal.UnixMilli(), "ts": oldmessage.TimestampReal.UnixMilli(),
@@ -374,7 +388,7 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
panic(err) panic(err)
} }
channelMap[intName] = channelID channelMap[strings.ToLower(intName)] = channelID
fmt.Printf("Auto Created Channel [%s]: %s\n", dispName, channelID) fmt.Printf("Auto Created Channel [%s]: %s\n", dispName, channelID)
@@ -383,6 +397,9 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
} }
sendername := determineSenderName(user, oldmessage, title, oldmessage.Content, channelInternalName) sendername := determineSenderName(user, oldmessage, title, oldmessage.Content, channelInternalName)
if sendername != nil && *sendername == "" {
panic("sendername")
}
if lastTitle == title && channelID == lastChannel && if lastTitle == title && channelID == lastChannel &&
langext.PtrEquals(lastContent, oldmessage.Content) && langext.PtrEquals(lastContent, oldmessage.Content) &&
@@ -394,7 +411,7 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
lastSendername = sendername lastSendername = sendername
lastTimestamp = oldmessage.TimestampReal lastTimestamp = oldmessage.TimestampReal
fmt.Printf("Skip message [%d] \"%s\" (fast-duplicate)\n", oldmessage.ScnMessageId, oldmessage.Title) //fmt.Printf("Skip message [%d] \"%s\" (fast-duplicate)\n", oldmessage.ScnMessageId, oldmessage.Title)
continue continue
} }
@@ -413,7 +430,7 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
lastSendername = sendername lastSendername = sendername
lastTimestamp = oldmessage.TimestampReal lastTimestamp = oldmessage.TimestampReal
fmt.Printf("Skip message [%d] \"%s\" (locally deleted in app)\n", oldmessage.ScnMessageId, oldmessage.Title) //fmt.Printf("Skip message [%d] \"%s\" (locally deleted in app)\n", oldmessage.ScnMessageId, oldmessage.Title)
continue continue
} }
} }
@@ -421,7 +438,7 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
pp := sq.PP{ pp := sq.PP{
"mid": messageid, "mid": messageid,
"suid": userid, "suid": userid,
"ouid": user.UserId, "ouid": userid,
"cnam": channelInternalName, "cnam": channelInternalName,
"cid": channelID, "cid": channelID,
"tsr": oldmessage.TimestampReal.UnixMilli(), "tsr": oldmessage.TimestampReal.UnixMilli(),
@@ -456,7 +473,7 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
_, err = dbnew.Exec(ctx, "INSERT INTO deliveries (delivery_id, message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (:did, :mid, :ruid, :rcid, :tsc, :tsf, :stat, :fcm, :next)", sq.PP{ _, err = dbnew.Exec(ctx, "INSERT INTO deliveries (delivery_id, message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (:did, :mid, :ruid, :rcid, :tsc, :tsf, :stat, :fcm, :next)", sq.PP{
"did": models.NewDeliveryID(), "did": models.NewDeliveryID(),
"mid": messageid, "mid": messageid,
"ruid": user.UserId, "ruid": userid,
"rcid": *clientid, "rcid": *clientid,
"tsc": oldmessage.TimestampReal.UnixMilli(), "tsc": oldmessage.TimestampReal.UnixMilli(),
"tsf": oldmessage.TimestampReal.UnixMilli(), "tsf": oldmessage.TimestampReal.UnixMilli(),
@@ -483,7 +500,7 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
_, err = dbnew.Exec(ctx, "INSERT INTO deliveries (delivery_id, message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (:did, :mid, :ruid, :rcid, :tsc, :tsf, :stat, :fcm, :next)", sq.PP{ _, err = dbnew.Exec(ctx, "INSERT INTO deliveries (delivery_id, message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (:did, :mid, :ruid, :rcid, :tsc, :tsf, :stat, :fcm, :next)", sq.PP{
"did": models.NewDeliveryID(), "did": models.NewDeliveryID(),
"mid": messageid, "mid": messageid,
"ruid": user.UserId, "ruid": userid,
"rcid": *clientid, "rcid": *clientid,
"tsc": oldmessage.TimestampReal.UnixMilli(), "tsc": oldmessage.TimestampReal.UnixMilli(),
"tsf": oldmessage.TimestampReal.UnixMilli(), "tsf": oldmessage.TimestampReal.UnixMilli(),
@@ -509,6 +526,92 @@ func migrateUser(ctx context.Context, dbnew sq.DB, dbold sq.DB, user OldUser, ap
lastTimestamp = oldmessage.TimestampReal lastTimestamp = oldmessage.TimestampReal
} }
{
c, err := dbnew.Query(ctx, "SELECT COUNT (*) FROM messages WHERE sender_user_id = :uid", sq.PP{
"uid": userid,
})
if err != nil {
panic(err)
}
if !c.Next() {
panic(false)
}
count := 0
err = c.Scan(&count)
if err != nil {
panic(err)
}
err = c.Close()
if err != nil {
panic(err)
}
if count > 0 {
_, err = dbnew.Exec(ctx, "UPDATE users SET messages_sent = :c, timestamp_lastread = :ts0, timestamp_lastsent = :ts1 WHERE user_id = :uid", sq.PP{
"uid": userid,
"c": count,
"ts0": lastTimestamp.UnixMilli(),
"ts1": lastTimestamp.UnixMilli(),
})
if err != nil {
panic(err)
}
} else {
_, err = dbnew.Exec(ctx, "UPDATE users SET messages_sent = :c, timestamp_lastread = :ts0 WHERE user_id = :uid", sq.PP{
"uid": userid,
"c": count,
"ts0": user.TimestampCreated.UnixMilli(),
})
if err != nil {
panic(err)
}
}
_, err = dbnew.Exec(ctx, "UPDATE keytokens SET messages_sent = :c WHERE owner_user_id = :uid", sq.PP{
"uid": userid,
"c": count,
})
if err != nil {
panic(err)
}
}
for _, cid := range channelMap {
c, err := dbnew.Query(ctx, "SELECT COUNT (*) FROM messages WHERE sender_user_id = :uid AND channel_id = :cid", sq.PP{
"cid": cid,
"uid": userid,
})
if err != nil {
panic(err)
}
if !c.Next() {
panic(false)
}
count := 0
err = c.Scan(&count)
if err != nil {
panic(err)
}
err = c.Close()
if err != nil {
panic(err)
}
_, err = dbnew.Exec(ctx, "UPDATE channels SET messages_sent = :c WHERE channel_id = :cid", sq.PP{
"cid": cid,
"c": count,
})
if err != nil {
panic(err)
}
}
} }
func determineSenderName(user OldUser, oldmessage OldMessage, title string, content *string, channame string) *string { func determineSenderName(user OldUser, oldmessage OldMessage, title string, content *string, channame string) *string {
@@ -516,6 +619,8 @@ func determineSenderName(user OldUser, oldmessage OldMessage, title string, cont
return nil return nil
} }
channame = strings.ToLower(channame)
if channame == "t-ctrl" { if channame == "t-ctrl" {
return langext.Ptr("sbox") return langext.Ptr("sbox")
} }
@@ -542,6 +647,42 @@ func determineSenderName(user OldUser, oldmessage OldMessage, title string, cont
if strings.Contains(title, "error on niflheim-3") { if strings.Contains(title, "error on niflheim-3") {
return langext.Ptr("niflheim-3") return langext.Ptr("niflheim-3")
} }
if strings.Contains(title, "error on statussrv") {
return langext.Ptr("statussrv")
}
if strings.Contains(title, "error on plan-web-prod") {
return langext.Ptr("plan-web-prod")
}
if strings.Contains(title, "error on inoshop") {
return langext.Ptr("inoshop")
}
if strings.Contains(title, "error on firestopcloud") {
return langext.Ptr("firestopcloud")
}
if strings.Contains(title, "error on plantafelstaging") {
return langext.Ptr("plantafelstaging")
}
if strings.Contains(title, "error on plantafeldev") {
return langext.Ptr("plantafeldev")
}
if strings.Contains(title, "error on balu-prod") {
return langext.Ptr("lbxprod")
}
if strings.Contains(title, "error on dyno-prod") {
return langext.Ptr("dyno-prod")
}
if strings.Contains(title, "error on dyno-dev") {
return langext.Ptr("dyno-dev")
}
if strings.Contains(title, "error on wkk") {
return langext.Ptr("wkk")
}
if strings.Contains(title, "error on lbxdev") {
return langext.Ptr("lbxdev")
}
if strings.Contains(title, "error on lbxprod") {
return langext.Ptr("lbxprod")
}
if strings.Contains(*content, "on mscom") { if strings.Contains(*content, "on mscom") {
return langext.Ptr("mscom") return langext.Ptr("mscom")
@@ -664,7 +805,7 @@ func determineSenderName(user OldUser, oldmessage OldMessage, title string, cont
return langext.Ptr("bfb") return langext.Ptr("bfb")
} }
if strings.Contains(title, "balu-db") { if strings.Contains(title, "balu-db") {
return langext.Ptr("lbprod") return langext.Ptr("lbxprod")
} }
} }
@@ -762,6 +903,31 @@ func determineSenderName(user OldUser, oldmessage OldMessage, title string, cont
if strings.Contains(*content, "plantafelstaging.de") { if strings.Contains(*content, "plantafelstaging.de") {
return langext.Ptr("plantafeldev") return langext.Ptr("plantafeldev")
} }
if strings.Contains(title, "Update cert_mscom") {
return langext.Ptr("mscom")
}
if strings.Contains(title, "Update cert_bfb") {
return langext.Ptr("bfb")
}
if strings.Contains(title, "Update staging.app.reuse.me") {
return langext.Ptr("wkk")
}
if strings.Contains(title, "Update develop.app.reuse.me") {
return langext.Ptr("wkk")
}
if strings.Contains(title, "Update inoshop_staging_bfb") {
return langext.Ptr("inoshop")
}
if strings.Contains(title, "Update cert_portfoliomanager_main") {
return langext.Ptr("bfb-testserver")
}
if strings.Contains(title, "Update cert_pfm2") {
return langext.Ptr("bfb-testserver")
}
if strings.Contains(title, "Update cert_kaz_main") {
return langext.Ptr("bfb-testserver")
}
} }
if channame == "space-warning" { if channame == "space-warning" {
@@ -777,6 +943,9 @@ func determineSenderName(user OldUser, oldmessage OldMessage, title string, cont
if title == "statussrv" { if title == "statussrv" {
return langext.Ptr("statussrv") return langext.Ptr("statussrv")
} }
if title == "virmach01" {
return langext.Ptr("statussrv")
}
} }
if channame == "srv-backup" { if channame == "srv-backup" {
@@ -856,6 +1025,28 @@ func determineSenderName(user OldUser, oldmessage OldMessage, title string, cont
if strings.Contains(title, "Reboot lbxprod") { if strings.Contains(title, "Reboot lbxprod") {
return langext.Ptr("lbxprod") return langext.Ptr("lbxprod")
} }
if strings.Contains(title, "Reboot plantafeldev") {
return langext.Ptr("plantafeldev")
}
if strings.Contains(title, "Reboot plantafelstaging") {
return langext.Ptr("plantafelstaging")
}
if strings.Contains(title, "Reboot statussrv") {
return langext.Ptr("statussrv")
}
if strings.Contains(title, "Reboot heydyno-prod-01") {
return langext.Ptr("dyno-prod")
}
if strings.Contains(title, "Reboot heydyno-dev-01") {
return langext.Ptr("dyno-dev")
}
if strings.Contains(title, "Reboot lbxdev") {
return langext.Ptr("lbxdev")
}
if strings.Contains(langext.Coalesce(content, ""), "Server: 'firestopcloud'") {
return langext.Ptr("firestopcloud")
}
} }
if channame == "yt-tvc" { if channame == "yt-tvc" {
@@ -870,6 +1061,124 @@ func determineSenderName(user OldUser, oldmessage OldMessage, title string, cont
return langext.Ptr("mscom") return langext.Ptr("mscom")
} }
if channame == "backup-rr" {
if strings.Contains(title, "bfb/server-plantafelstaging") {
return langext.Ptr("plantafelstaging")
}
if strings.Contains(title, "bfb/server-plantafeldev") {
return langext.Ptr("plantafeldev")
}
if strings.Contains(title, "bfb/server-wkk") {
return langext.Ptr("wkk")
}
if strings.Contains(title, "bfb/holz100") {
return langext.Ptr("bfb")
}
if strings.Contains(title, "bfb/clockify") {
return langext.Ptr("bfb")
}
if strings.Contains(title, "bfb/gdapi") {
return langext.Ptr("bfb")
}
if strings.Contains(title, "mike/databases-mscom") {
return langext.Ptr("mscom")
}
if strings.Contains(title, "bfb/databases-bfb") {
return langext.Ptr("bfb")
}
if strings.Contains(title, "bfb/server-dynoprod") {
return langext.Ptr("dyno-prod")
}
if strings.Contains(title, "bfb/server-dynodev") {
return langext.Ptr("dyno-dev")
}
if strings.Contains(title, "bfb/server-baluprod") {
return langext.Ptr("lbxprod")
}
if strings.Contains(title, "bfb/server-baludev") {
return langext.Ptr("lbxdev")
}
if strings.Contains(title, "bfb/plantafeldigital") {
return langext.Ptr("plan-web-prod")
}
if strings.Contains(title, "mike/server-statussrv") {
return langext.Ptr("statussrv")
}
if strings.Contains(title, "mike/thunderbird") {
return langext.Ptr("niflheim-3")
}
if strings.Contains(title, "mike/seedbox") {
return langext.Ptr("sbox")
}
if strings.Contains(title, "bfb/balu") {
return langext.Ptr("lbxprod")
}
if strings.Contains(title, "mike/ext-git-graph") {
return langext.Ptr("mscom")
}
if strings.Contains(title, "bfb/server-bfbtest") {
return langext.Ptr("bfb-testserver")
}
if strings.Contains(title, "mike/server-mscom") {
return langext.Ptr("mscom")
}
if strings.Contains(title, "mike/database-statussrv") {
return langext.Ptr("statussrv")
}
if strings.Contains(title, "mike/server-mscom") {
return langext.Ptr("mscom")
}
if strings.Contains(title, "mike/server-inoshop") {
return langext.Ptr("inoshop")
}
if strings.Contains(title, "mike/server-bfb") {
return langext.Ptr("bfb")
}
if strings.Contains(title, "bfb/discord") {
return langext.Ptr("nifleim-3")
}
if strings.Contains(title, "bfb/server-bfbtest") {
return langext.Ptr("bfb-testserver")
}
if strings.Contains(title, "mike/ext-git-graph") {
return langext.Ptr("mscom")
}
if strings.Contains(title, "bfb/server-inoshop") {
return langext.Ptr("inoshop")
}
if strings.Contains(title, "bfb/server-bfb") {
return langext.Ptr("bfb")
}
if strings.Contains(title, "bfb/server-plantafelprod") {
return langext.Ptr("plan-web-prod")
}
if strings.Contains(title, "bfb/server-inoshop") {
return langext.Ptr("inoshop")
}
if strings.Contains(title, "mike/niflheim-3") {
return langext.Ptr("niflheim-3")
}
if strings.Contains(title, "mike/pihole") {
return langext.Ptr("pihole")
}
if strings.Contains(title, "bfb/server-firestopcloud") {
return langext.Ptr("firestopcloud")
}
if strings.Contains(title, "bfb/server-agentzero") {
return langext.Ptr("agentzero")
}
}
if channame == "backup" {
if strings.Contains(title, "mike/ext-git-graph") {
return langext.Ptr("mscom")
}
}
if channame == "spezi-alert" {
return langext.Ptr("mscom")
}
if title == "NCC Upload failed" || title == "NCC Upload successful" { if title == "NCC Upload failed" || title == "NCC Upload successful" {
return langext.Ptr("mscom") return langext.Ptr("mscom")
} }
@@ -878,16 +1187,29 @@ func determineSenderName(user OldUser, oldmessage OldMessage, title string, cont
return langext.Ptr("mscom") return langext.Ptr("mscom")
} }
if strings.Contains(title, "BFBackup VC migrate") {
return langext.Ptr("bfbackup")
}
if strings.Contains(title, "bfbackup job") { if strings.Contains(title, "bfbackup job") {
return langext.Ptr("bfbackup") return langext.Ptr("bfbackup")
} }
if strings.Contains(title, "bfbackup finished") {
return langext.Ptr("bfbackup")
}
if strings.Contains(title, "Repo migration of /volume1") { if strings.Contains(title, "Repo migration of /volume1") {
return langext.Ptr("bfbackup") return langext.Ptr("bfbackup")
} }
if channame == "docker-watch" {
return nil // okay
}
//fmt.Printf("Failed to determine sender of [%d] '%s' '%s'\n", oldmessage.ScnMessageId, oldmessage.Title, langext.Coalesce(oldmessage.Content, "<NULL>")) //fmt.Printf("Failed to determine sender of [%d] '%s' '%s'\n", oldmessage.ScnMessageId, oldmessage.Title, langext.Coalesce(oldmessage.Content, "<NULL>"))
fmt.Printf("Failed to determine sender of [%d] '%s'\n", oldmessage.ScnMessageId, oldmessage.Title)
fmt.Printf("%s", termext.Red(fmt.Sprintf("Failed to determine sender of [%d] '%s' -- '%s' -- '%s'\n", oldmessage.ScnMessageId, channame, title, langext.Coalesce(oldmessage.Content, "<NULL>"))))
return nil return nil
} }

View File

@@ -268,6 +268,7 @@ var configDev = func() Config {
GooglePackageName: confEnv("SCN_GOOG_PACKAGENAME"), GooglePackageName: confEnv("SCN_GOOG_PACKAGENAME"),
GoogleProProductID: confEnv("SCN_GOOG_PROPRODUCTID"), GoogleProProductID: confEnv("SCN_GOOG_PROPRODUCTID"),
Cors: true, Cors: true,
ReqLogEnabled: true,
ReqLogMaxBodySize: 2048, ReqLogMaxBodySize: 2048,
ReqLogHistoryMaxCount: 1638, ReqLogHistoryMaxCount: 1638,
ReqLogHistoryMaxDuration: timeext.FromDays(60), ReqLogHistoryMaxDuration: timeext.FromDays(60),
@@ -339,6 +340,7 @@ var configStag = func() Config {
GooglePackageName: confEnv("SCN_GOOG_PACKAGENAME"), GooglePackageName: confEnv("SCN_GOOG_PACKAGENAME"),
GoogleProProductID: confEnv("SCN_GOOG_PROPRODUCTID"), GoogleProProductID: confEnv("SCN_GOOG_PROPRODUCTID"),
Cors: true, Cors: true,
ReqLogEnabled: true,
ReqLogMaxBodySize: 2048, ReqLogMaxBodySize: 2048,
ReqLogHistoryMaxCount: 1638, ReqLogHistoryMaxCount: 1638,
ReqLogHistoryMaxDuration: timeext.FromDays(60), ReqLogHistoryMaxDuration: timeext.FromDays(60),
@@ -398,18 +400,19 @@ var configProd = func() Config {
ReturnRawErrors: false, ReturnRawErrors: false,
DummyFirebase: false, DummyFirebase: false,
FirebaseTokenURI: "https://oauth2.googleapis.com/token", FirebaseTokenURI: "https://oauth2.googleapis.com/token",
FirebaseProjectID: confEnv("SCN_SCN_FB_PROJECTID"), FirebaseProjectID: confEnv("SCN_FB_PROJECTID"),
FirebasePrivKeyID: confEnv("SCN_SCN_FB_PRIVATEKEYID"), FirebasePrivKeyID: confEnv("SCN_FB_PRIVATEKEYID"),
FirebaseClientMail: confEnv("SCN_SCN_FB_CLIENTEMAIL"), FirebaseClientMail: confEnv("SCN_FB_CLIENTEMAIL"),
FirebasePrivateKey: confEnv("SCN_SCN_FB_PRIVATEKEY"), FirebasePrivateKey: confEnv("SCN_FB_PRIVATEKEY"),
DummyGoogleAPI: false, DummyGoogleAPI: false,
GoogleAPITokenURI: "https://oauth2.googleapis.com/token", GoogleAPITokenURI: "https://oauth2.googleapis.com/token",
GoogleAPIPrivKeyID: confEnv("SCN_SCN_GOOG_PRIVATEKEYID"), GoogleAPIPrivKeyID: confEnv("SCN_GOOG_PRIVATEKEYID"),
GoogleAPIClientMail: confEnv("SCN_SCN_GOOG_CLIENTEMAIL"), GoogleAPIClientMail: confEnv("SCN_GOOG_CLIENTEMAIL"),
GoogleAPIPrivateKey: confEnv("SCN_SCN_GOOG_PRIVATEKEY"), GoogleAPIPrivateKey: confEnv("SCN_GOOG_PRIVATEKEY"),
GooglePackageName: confEnv("SCN_SCN_GOOG_PACKAGENAME"), GooglePackageName: confEnv("SCN_GOOG_PACKAGENAME"),
GoogleProProductID: confEnv("SCN_SCN_GOOG_PROPRODUCTID"), GoogleProProductID: confEnv("SCN_GOOG_PROPRODUCTID"),
Cors: true, Cors: true,
ReqLogEnabled: true,
ReqLogMaxBodySize: 2048, ReqLogMaxBodySize: 2048,
ReqLogHistoryMaxCount: 1638, ReqLogHistoryMaxCount: 1638,
ReqLogHistoryMaxDuration: timeext.FromDays(60), ReqLogHistoryMaxDuration: timeext.FromDays(60),
@@ -449,7 +452,7 @@ func confEnv(key string) string {
} }
func init() { func init() {
ns := os.Getenv("SCN_NAMESPACE") ns := os.Getenv("CONF_NS")
cfg, ok := GetConfig(ns) cfg, ok := GetConfig(ns)
if !ok { if !ok {

View File

@@ -1,7 +1,6 @@
package primary package db
import ( import (
"blackforestbytes.com/simplecloudnotifier/db"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "time"
) )
@@ -12,5 +11,5 @@ type TxContext interface {
Err() error Err() error
Value(key any) any 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) BeginTx(ctx context.Context) (sq.Tx, error)
Stop(ctx context.Context) 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 WriteMetaString(ctx TxContext, key string, value string) error
WriteMetaInt(ctx context.Context, key string, value int64) error WriteMetaInt(ctx TxContext, key string, value int64) error
WriteMetaReal(ctx context.Context, key string, value float64) error WriteMetaReal(ctx TxContext, key string, value float64) error
WriteMetaBlob(ctx context.Context, key string, value []byte) error WriteMetaBlob(ctx TxContext, key string, value []byte) error
ReadMetaString(ctx context.Context, key string) (*string, error) ReadMetaString(ctx TxContext, key string) (*string, error)
ReadMetaInt(ctx context.Context, key string) (*int64, error) ReadMetaInt(ctx TxContext, key string) (*int64, error)
ReadMetaReal(ctx context.Context, key string) (*float64, error) ReadMetaReal(ctx TxContext, key string) (*float64, error)
ReadMetaBlob(ctx context.Context, key string) (*[]byte, 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" server "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db/dbtools" "blackforestbytes.com/simplecloudnotifier/db/dbtools"
"blackforestbytes.com/simplecloudnotifier/db/schema" "blackforestbytes.com/simplecloudnotifier/db/schema"
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
@@ -63,77 +64,93 @@ func (db *Database) DB() sq.DB {
return db.db return db.db
} }
func (db *Database) Migrate(ctx context.Context) error { func (db *Database) Migrate(outerctx context.Context) error {
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second) innerctx, cancel := context.WithTimeout(outerctx, 24*time.Second)
defer cancel() 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 { if err != nil {
return err return err
} }
if currschema == 0 { if currschema == 0 {
schemastr := schema.LogsSchema[schema.LogsSchemaVersion].SQL
schemahash := schema.LogsSchema[schema.LogsSchemaVersion].Hash
schemastr := schema.LogsSchema1 _, err = tx.Exec(tctx, schemastr, sq.PP{})
schemahash, err := sq.HashSqliteSchema(ctx, schemastr)
if err != nil { if err != nil {
return err return err
} }
_, err = db.db.Exec(ctx, schemastr, sq.PP{}) err = db.WriteMetaInt(tctx, "schema", int64(schema.LogsSchemaVersion))
if err != nil { if err != nil {
return err return err
} }
err = db.WriteMetaInt(ctx, "schema", 1) err = db.WriteMetaString(tctx, "schema_hash", schemahash)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
err = db.pp.Init(ctx) // Re-Init schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
if err != nil { if err != nil {
return err return err
} }
return nil if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.LogsSchema[currschema].Hash {
} 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 {
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (logs db)") log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (logs db)")
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).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)") return errors.New("database schema does not match (logs db)")
} else { } else {
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (logs db)") log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (logs db)")
} }
}
return nil // current if currschema != schema.LogsSchemaVersion {
} else {
return errors.New(fmt.Sprintf("Unknown DB schema: %d", currschema)) 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 { func (db *Database) Ping(ctx context.Context) error {

View File

@@ -1,15 +1,19 @@
package logs package logs
import ( import (
"context" "blackforestbytes.com/simplecloudnotifier/db"
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "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 { if err != nil {
return 0, err return 0, err
} }
@@ -31,7 +35,7 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return 0, err 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 { if err != nil {
return 0, err return 0, err
} }
@@ -62,8 +66,13 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return dbschema, nil return dbschema, nil
} }
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error { func (db *Database) WriteMetaString(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -73,8 +82,13 @@ func (db *Database) WriteMetaString(ctx context.Context, key string, value strin
return nil return nil
} }
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error { func (db *Database) WriteMetaInt(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -84,8 +98,13 @@ func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) e
return nil return nil
} }
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error { func (db *Database) WriteMetaReal(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -95,8 +114,13 @@ func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64
return nil return nil
} }
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error { func (db *Database) WriteMetaBlob(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -106,8 +130,13 @@ func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte)
return nil return nil
} }
func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *string, reterr error) { func (db *Database) ReadMetaString(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -137,8 +166,13 @@ func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *str
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64, reterr error) { func (db *Database) ReadMetaInt(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -169,8 +203,13 @@ func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64,
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float64, reterr error) { func (db *Database) ReadMetaReal(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -201,8 +240,13 @@ func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byte, reterr error) { func (db *Database) ReadMetaBlob(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -233,8 +277,13 @@ func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byt
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) DeleteMeta(ctx context.Context, key string) error { func (db *Database) DeleteMeta(ctx db.TxContext, key string) error {
_, err := db.db.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key}) 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 { if err != nil {
return err return err
} }

View File

@@ -1,13 +1,15 @@
package primary package primary
import ( import (
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -22,7 +24,7 @@ func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanNa
} }
channel, err := models.DecodeChannel(rows) channel, err := models.DecodeChannel(rows)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, nil
} }
if err != nil { if err != nil {
@@ -32,7 +34,7 @@ func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanNa
return &channel, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -46,7 +48,7 @@ func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*mod
} }
channel, err := models.DecodeChannel(rows) channel, err := models.DecodeChannel(rows)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, nil
} }
if err != nil { if err != nil {
@@ -56,7 +58,7 @@ func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*mod
return &channel, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Channel{}, err return models.Channel{}, err
@@ -81,7 +83,7 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName
return entity.Model(), nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -105,7 +107,7 @@ func (db *Database) ListChannelsByOwner(ctx TxContext, userid models.UserID, sub
return data, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -135,7 +137,7 @@ func (db *Database) ListChannelsBySubscriber(ctx TxContext, userid models.UserID
return data, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -166,7 +168,7 @@ func (db *Database) ListChannelsByAccess(ctx TxContext, userid models.UserID, co
return data, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.ChannelWithSubscription{}, err return models.ChannelWithSubscription{}, err
@@ -200,7 +202,7 @@ func (db *Database) GetChannel(ctx TxContext, userid models.UserID, channelid mo
return channel, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -222,7 +224,7 @@ func (db *Database) IncChannelMessageCounter(ctx TxContext, channel *models.Chan
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -239,7 +241,7 @@ func (db *Database) UpdateChannelSubscribeKey(ctx TxContext, channelid models.Ch
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -256,7 +258,7 @@ func (db *Database) UpdateChannelDisplayName(ctx TxContext, channelid models.Cha
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err

View File

@@ -1,12 +1,13 @@
package primary package primary
import ( import (
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Client{}, err return models.Client{}, err
@@ -30,7 +31,7 @@ func (db *Database) CreateClient(ctx TxContext, userid models.UserID, ctype mode
return entity.Model(), nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -44,7 +45,7 @@ func (db *Database) ClearFCMTokens(ctx TxContext, fcmtoken string) error {
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -63,7 +64,7 @@ func (db *Database) ListClients(ctx TxContext, userid models.UserID) ([]models.C
return data, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Client{}, err return models.Client{}, err
@@ -85,7 +86,7 @@ func (db *Database) GetClient(ctx TxContext, userid models.UserID, clientid mode
return client, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -99,7 +100,7 @@ func (db *Database) DeleteClient(ctx TxContext, clientid models.ClientID) error
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -113,7 +114,7 @@ func (db *Database) DeleteClientsByFCM(ctx TxContext, fcmtoken string) error {
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -130,7 +131,7 @@ func (db *Database) UpdateClientFCMToken(ctx TxContext, clientid models.ClientID
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -147,7 +148,7 @@ func (db *Database) UpdateClientAgentModel(ctx TxContext, clientid models.Client
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err

View File

@@ -1,13 +1,14 @@
package primary package primary
import ( import (
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -42,7 +43,7 @@ func (db *Database) CreateCompatID(ctx TxContext, idtype string, newid string) (
return oldid, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -62,7 +63,7 @@ func (db *Database) ConvertCompatID(ctx TxContext, oldid int64, idtype string) (
var newid string var newid string
err = rows.Scan(&newid) err = rows.Scan(&newid)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, nil
} }
if err != nil { if err != nil {
@@ -72,7 +73,7 @@ func (db *Database) ConvertCompatID(ctx TxContext, oldid int64, idtype string) (
return &newid, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -90,7 +91,7 @@ func (db *Database) ConvertToCompatID(ctx TxContext, newid string) (*int64, *str
var oldid int64 var oldid int64
var idtype string var idtype string
err = rows.Scan(&oldid, &idtype) err = rows.Scan(&oldid, &idtype)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return nil, nil, nil return nil, nil, nil
} }
if err != nil { if err != nil {
@@ -100,7 +101,7 @@ func (db *Database) ConvertToCompatID(ctx TxContext, newid string) (*int64, *str
return &oldid, &idtype, nil 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) id1, _, err := db.ConvertToCompatID(ctx, newid)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -116,7 +117,7 @@ func (db *Database) ConvertToCompatIDOrCreate(ctx TxContext, idtype string, newi
return id2, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return false, err return false, err
@@ -139,7 +140,7 @@ func (db *Database) GetAck(ctx TxContext, msgid models.MessageID) (bool, error)
return res, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -156,7 +157,7 @@ func (db *Database) SetAck(ctx TxContext, userid models.UserID, msgid models.Mes
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return false, err return false, err

View File

@@ -4,6 +4,7 @@ import (
server "blackforestbytes.com/simplecloudnotifier" server "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db/dbtools" "blackforestbytes.com/simplecloudnotifier/db/dbtools"
"blackforestbytes.com/simplecloudnotifier/db/schema" "blackforestbytes.com/simplecloudnotifier/db/schema"
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
@@ -63,81 +64,147 @@ func (db *Database) DB() sq.DB {
return db.db return db.db
} }
func (db *Database) Migrate(ctx context.Context) error { func (db *Database) Migrate(outerctx context.Context) error {
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second) innerctx, cancel := context.WithTimeout(outerctx, 24*time.Second)
defer cancel() 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 { if err != nil {
return err return err
} }
if currschema == 0 { if currschema == 0 {
schemastr := schema.PrimarySchema[schema.PrimarySchemaVersion].SQL
schemahash := schema.PrimarySchema[schema.PrimarySchemaVersion].Hash
schemastr := schema.PrimarySchema3 _, err = tx.Exec(tctx, schemastr, sq.PP{})
schemahash, err := sq.HashSqliteSchema(ctx, schemastr)
if err != nil { if err != nil {
return err return err
} }
_, err = db.db.Exec(ctx, schemastr, sq.PP{}) err = db.WriteMetaInt(tctx, "schema", int64(schema.PrimarySchemaVersion))
if err != nil { if err != nil {
return err return err
} }
err = db.WriteMetaInt(ctx, "schema", 3) err = db.WriteMetaString(tctx, "schema_hash", schemahash)
if err != nil { if err != nil {
return err return err
} }
err = db.WriteMetaString(ctx, "schema_hash", schemahash) ppReInit = true
if err != nil {
return err
}
err = db.pp.Init(ctx) // Re-Init currschema = schema.PrimarySchemaVersion
if err != nil { }
return err
}
return nil if currschema == 1 {
} else if currschema == 1 {
return errors.New("cannot autom. upgrade schema 1") return errors.New("cannot autom. upgrade schema 1")
} else if currschema == 2 { }
if currschema == 2 {
return errors.New("cannot autom. upgrade schema 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 { if err != nil {
return err return err
} }
schemaHashMeta, err := db.ReadMetaString(ctx, "schema_hash") schemHashDB, err := sq.HashSqliteDatabase(tctx, tx)
if err != nil { if err != nil {
return err return err
} }
schemHashAsset := schema.PrimaryHash3 if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.PrimarySchema[currschema].Hash {
if err != nil {
return err
}
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schemHashAsset {
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (primary db)") log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (primary db)")
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).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)") return errors.New("database schema does not match (primary db)")
} else { } else {
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (primary db)") 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")
} else {
_, 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)) 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 { func (db *Database) Ping(ctx context.Context) error {

View File

@@ -2,13 +2,14 @@ package primary
import ( import (
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Delivery{}, err return models.Delivery{}, err
@@ -38,7 +39,7 @@ func (db *Database) CreateRetryDelivery(ctx TxContext, client models.Client, msg
return entity.Model(), nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Delivery{}, err return models.Delivery{}, err
@@ -67,7 +68,7 @@ func (db *Database) CreateSuccessDelivery(ctx TxContext, client models.Client, m
return entity.Model(), nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -89,7 +90,7 @@ func (db *Database) ListRetrieableDeliveries(ctx TxContext, pageSize int) ([]mod
return data, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -108,7 +109,7 @@ func (db *Database) SetDeliverySuccess(ctx TxContext, delivery models.Delivery,
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -127,7 +128,7 @@ func (db *Database) SetDeliveryFailed(ctx TxContext, delivery models.Delivery) e
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -145,7 +146,7 @@ func (db *Database) SetDeliveryRetry(ctx TxContext, delivery models.Delivery) er
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err

View File

@@ -1,15 +1,17 @@
package primary package primary
import ( import (
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"strings" "strings"
"time" "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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.KeyToken{}, err return models.KeyToken{}, err
@@ -36,7 +38,7 @@ func (db *Database) CreateKeyToken(ctx TxContext, name string, owner models.User
return entity.Model(), nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -55,7 +57,7 @@ func (db *Database) ListKeyTokens(ctx TxContext, ownerID models.UserID) ([]model
return data, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.KeyToken{}, err return models.KeyToken{}, err
@@ -77,7 +79,7 @@ func (db *Database) GetKeyToken(ctx TxContext, userid models.UserID, keyTokenid
return keyToken, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -89,7 +91,7 @@ func (db *Database) GetKeyTokenByToken(ctx TxContext, key string) (*models.KeyTo
} }
user, err := models.DecodeKeyToken(rows) user, err := models.DecodeKeyToken(rows)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, nil
} }
if err != nil { if err != nil {
@@ -99,7 +101,7 @@ func (db *Database) GetKeyTokenByToken(ctx TxContext, key string) (*models.KeyTo
return &user, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -113,7 +115,7 @@ func (db *Database) DeleteKeyToken(ctx TxContext, keyTokenid models.KeyTokenID)
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -130,7 +132,7 @@ func (db *Database) UpdateKeyTokenName(ctx TxContext, keyTokenid models.KeyToken
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -147,7 +149,7 @@ func (db *Database) UpdateKeyTokenPermissions(ctx TxContext, keyTokenid models.K
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -164,7 +166,7 @@ func (db *Database) UpdateKeyTokenAllChannels(ctx TxContext, keyTokenid models.K
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -181,7 +183,7 @@ func (db *Database) UpdateKeyTokenChannels(ctx TxContext, keyTokenid models.KeyT
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -203,7 +205,7 @@ func (db *Database) IncKeyTokenMessageCounter(ctx TxContext, keyToken *models.Ke
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err

View File

@@ -1,14 +1,16 @@
package primary package primary
import ( import (
"blackforestbytes.com/simplecloudnotifier/db"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken" ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -20,7 +22,7 @@ func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*
} }
msg, err := models.DecodeMessage(rows) msg, err := models.DecodeMessage(rows)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, nil
} }
if err != nil { if err != nil {
@@ -30,7 +32,7 @@ func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*
return &msg, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Message{}, err return models.Message{}, err
@@ -56,7 +58,7 @@ func (db *Database) GetMessage(ctx TxContext, scnMessageID models.MessageID, all
return msg, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Message{}, err return models.Message{}, err
@@ -65,7 +67,6 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
entity := models.MessageDB{ entity := models.MessageDB{
MessageID: models.NewMessageID(), MessageID: models.NewMessageID(),
SenderUserID: senderUserID, SenderUserID: senderUserID,
OwnerUserID: channel.OwnerUserID,
ChannelInternalName: channel.InternalName, ChannelInternalName: channel.InternalName,
ChannelID: channel.ChannelID, ChannelID: channel.ChannelID,
SenderIP: senderIP, SenderIP: senderIP,
@@ -88,7 +89,7 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
return entity.Model(), nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -102,7 +103,7 @@ func (db *Database) DeleteMessage(ctx TxContext, messageID models.MessageID) err
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, ct.CursorToken{}, err return nil, ct.CursorToken{}, err
@@ -149,3 +150,31 @@ func (db *Database) ListMessages(ctx TxContext, filter models.MessageFilter, pag
return data[0:*pageSize], outToken, nil return data[0:*pageSize], outToken, nil
} }
} }
func (db *Database) CountMessages(ctx db.TxContext, filter models.MessageFilter) (int64, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return 0, err
}
filterCond, filterJoin, prepParams, err := filter.SQL()
sqlQuery := "SELECT " + "COUNT(*)" + " FROM messages " + filterJoin + " WHERE ( " + filterCond + " ) "
rows, err := tx.Query(ctx, sqlQuery, prepParams)
if err != nil {
return 0, err
}
if !rows.Next() {
return 0, errors.New("COUNT query returned no results")
}
var countRes int64
err = rows.Scan(&countRes)
if err != nil {
return 0, err
}
return countRes, nil
}

View File

@@ -1,15 +1,19 @@
package primary package primary
import ( import (
"context" "blackforestbytes.com/simplecloudnotifier/db"
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "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 { if err != nil {
return 0, err return 0, err
} }
@@ -31,7 +35,7 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return 0, err 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 { if err != nil {
return 0, err return 0, err
} }
@@ -62,8 +66,13 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return dbschema, nil return dbschema, nil
} }
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error { func (db *Database) WriteMetaString(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -73,8 +82,13 @@ func (db *Database) WriteMetaString(ctx context.Context, key string, value strin
return nil return nil
} }
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error { func (db *Database) WriteMetaInt(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -84,8 +98,13 @@ func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) e
return nil return nil
} }
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error { func (db *Database) WriteMetaReal(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -95,8 +114,13 @@ func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64
return nil return nil
} }
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error { func (db *Database) WriteMetaBlob(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -106,8 +130,13 @@ func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte)
return nil return nil
} }
func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *string, reterr error) { func (db *Database) ReadMetaString(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -137,8 +166,13 @@ func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *str
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64, reterr error) { func (db *Database) ReadMetaInt(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -169,8 +203,13 @@ func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64,
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float64, reterr error) { func (db *Database) ReadMetaReal(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -201,8 +240,13 @@ func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byte, reterr error) { func (db *Database) ReadMetaBlob(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -233,8 +277,13 @@ func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byt
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) DeleteMeta(ctx context.Context, key string) error { func (db *Database) DeleteMeta(ctx db.TxContext, key string) error {
_, err := db.db.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key}) 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 { if err != nil {
return err return err
} }

View File

@@ -1,13 +1,15 @@
package primary package primary
import ( import (
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Subscription{}, err return models.Subscription{}, err
@@ -31,15 +33,19 @@ func (db *Database) CreateSubscription(ctx TxContext, subscriberUID models.UserI
return entity.Model(), nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@@ -52,63 +58,7 @@ func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID models.C
return data, nil return data, nil
} }
func (db *Database) ListSubscriptionsByChannelOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]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 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) {
tx, err := ctx.GetOrCreateTransaction(db) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Subscription{}, err return models.Subscription{}, err
@@ -127,7 +77,7 @@ func (db *Database) GetSubscription(ctx TxContext, subid models.SubscriptionID)
return sub, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -142,7 +92,7 @@ func (db *Database) GetSubscriptionBySubscriber(ctx TxContext, subscriberId mode
} }
user, err := models.DecodeSubscription(rows) user, err := models.DecodeSubscription(rows)
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, nil
} }
if err != nil { if err != nil {
@@ -152,7 +102,7 @@ func (db *Database) GetSubscriptionBySubscriber(ctx TxContext, subscriberId mode
return &user, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -166,7 +116,7 @@ func (db *Database) DeleteSubscription(ctx TxContext, subid models.SubscriptionI
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err

View File

@@ -2,13 +2,14 @@ package primary
import ( import (
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.User{}, err return models.User{}, err
@@ -35,7 +36,7 @@ func (db *Database) CreateUser(ctx TxContext, protoken *string, username *string
return entity.Model(), nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -49,7 +50,7 @@ func (db *Database) ClearProTokens(ctx TxContext, protoken string) error {
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.User{}, err return models.User{}, err
@@ -68,7 +69,7 @@ func (db *Database) GetUser(ctx TxContext, userid models.UserID) (models.User, e
return user, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -85,7 +86,7 @@ func (db *Database) UpdateUserUsername(ctx TxContext, userid models.UserID, user
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -103,7 +104,7 @@ func (db *Database) UpdateUserProToken(ctx TxContext, userid models.UserID, prot
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err
@@ -132,7 +133,7 @@ func (db *Database) IncUserMessageCounter(ctx TxContext, user *models.User) erro
return nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return err return err

View File

@@ -4,6 +4,7 @@ import (
server "blackforestbytes.com/simplecloudnotifier" server "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db/dbtools" "blackforestbytes.com/simplecloudnotifier/db/dbtools"
"blackforestbytes.com/simplecloudnotifier/db/schema" "blackforestbytes.com/simplecloudnotifier/db/schema"
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
@@ -63,77 +64,98 @@ func (db *Database) DB() sq.DB {
return db.db return db.db
} }
func (db *Database) Migrate(ctx context.Context) error { func (db *Database) Migrate(outerctx context.Context) error {
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second) innerctx, cancel := context.WithTimeout(outerctx, 24*time.Second)
defer cancel() 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 { if err != nil {
return err return err
} }
if currschema == 0 { if currschema == 0 {
schemastr := schema.RequestsSchema[schema.RequestsSchemaVersion].SQL
schemahash := schema.RequestsSchema[schema.RequestsSchemaVersion].Hash
schemastr := schema.RequestsSchema1 schemahash, err := sq.HashSqliteSchema(tctx, schemastr)
schemahash, err := sq.HashSqliteSchema(ctx, schemastr)
if err != nil { if err != nil {
return err return err
} }
_, err = db.db.Exec(ctx, schemastr, sq.PP{}) _, err = tx.Exec(tctx, schemastr, sq.PP{})
if err != nil { if err != nil {
return err return err
} }
err = db.WriteMetaInt(ctx, "schema", 1) err = db.WriteMetaInt(tctx, "schema", int64(schema.RequestsSchemaVersion))
if err != nil { if err != nil {
return err return err
} }
err = db.WriteMetaString(ctx, "schema_hash", schemahash) err = db.WriteMetaString(tctx, "schema_hash", schemahash)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
return nil schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
} else if currschema == 1 {
schemHashDB, err := sq.HashSqliteDatabase(ctx, db.db)
if err != nil { if err != nil {
return err return err
} }
schemaHashMeta, err := db.ReadMetaString(ctx, "schema_hash") if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.RequestsSchema[currschema].Hash {
if err != nil {
return err
}
schemHashAsset := schema.RequestsHash1
if err != nil {
return err
}
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schemHashAsset {
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (requests db)") log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (requests db)")
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).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)") return errors.New("database schema does not match (requests db)")
} else { } else {
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (requests db)") log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (requests db)")
} }
}
return nil // current if currschema != schema.RequestsSchemaVersion {
} else {
return errors.New(fmt.Sprintf("Unknown DB schema: %d", currschema)) 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 { func (db *Database) Ping(ctx context.Context) error {

View File

@@ -1,15 +1,19 @@
package requests package requests
import ( import (
"context" "blackforestbytes.com/simplecloudnotifier/db"
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "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 { if err != nil {
return 0, err return 0, err
} }
@@ -31,7 +35,7 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return 0, err 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 { if err != nil {
return 0, err return 0, err
} }
@@ -62,8 +66,13 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return dbschema, nil return dbschema, nil
} }
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error { func (db *Database) WriteMetaString(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -73,8 +82,13 @@ func (db *Database) WriteMetaString(ctx context.Context, key string, value strin
return nil return nil
} }
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error { func (db *Database) WriteMetaInt(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -84,8 +98,13 @@ func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) e
return nil return nil
} }
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error { func (db *Database) WriteMetaReal(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -95,8 +114,13 @@ func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64
return nil return nil
} }
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error { func (db *Database) WriteMetaBlob(ctx db.TxContext, 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{ 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, "key": key,
"val": value, "val": value,
}) })
@@ -106,8 +130,13 @@ func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte)
return nil return nil
} }
func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *string, reterr error) { func (db *Database) ReadMetaString(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -137,8 +166,13 @@ func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *str
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64, reterr error) { func (db *Database) ReadMetaInt(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -169,8 +203,13 @@ func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64,
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float64, reterr error) { func (db *Database) ReadMetaReal(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -201,8 +240,13 @@ func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byte, reterr error) { func (db *Database) ReadMetaBlob(ctx db.TxContext, 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}) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -233,8 +277,13 @@ func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byt
return langext.Ptr(value), nil return langext.Ptr(value), nil
} }
func (db *Database) DeleteMeta(ctx context.Context, key string) error { func (db *Database) DeleteMeta(ctx db.TxContext, key string) error {
_, err := db.db.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key}) 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 { if err != nil {
return err return err
} }

View File

@@ -2,27 +2,52 @@ package schema
import _ "embed" import _ "embed"
//go:embed primary_1.ddl type Def struct {
var PrimarySchema1 string SQL string
Hash string
}
const PrimaryHash1 = "f2b2847f32681a7178e405553beea4a324034915a0c5a5dc70b3c6abbcc852f2" //go:embed primary_1.ddl
var primarySchema1 string
//go:embed primary_2.ddl //go:embed primary_2.ddl
var PrimarySchema2 string var primarySchema2 string
const PrimaryHash2 = "07ed1449114416ed043084a30e0722a5f97bf172161338d2f7106a8dfd387d0a"
//go:embed primary_3.ddl //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 //go:embed requests_1.ddl
var RequestsSchema1 string var requestsSchema1 string
const RequestsHash1 = "ebb0a5748b605e8215437413b738279670190ca8159b6227cfc2aa13418b41e9"
//go:embed logs_1.ddl //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 ( import (
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
@@ -51,7 +51,9 @@ func (sc *SimpleContext) Cancel() {
} }
sc.transaction = nil sc.transaction = nil
} }
sc.cancelFunc() if sc.cancelFunc != nil {
sc.cancelFunc()
}
} }
func (sc *SimpleContext) GetOrCreateTransaction(db db.DatabaseImpl) (sq.Tx, error) { func (sc *SimpleContext) GetOrCreateTransaction(db db.DatabaseImpl) (sq.Tx, error) {

View File

@@ -4,19 +4,20 @@ go 1.19
require ( require (
github.com/gin-gonic/gin v1.9.1 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/go-sql-driver/mysql v1.7.1
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-sqlite3 v1.14.17 github.com/mattn/go-sqlite3 v1.14.17
github.com/rs/zerolog v1.29.1 github.com/rs/zerolog v1.31.0
gogs.mikescher.com/BlackForestBytes/goext v0.0.156 gogs.mikescher.com/BlackForestBytes/goext v0.0.291
gopkg.in/loremipsum.v1 v1.1.2 gopkg.in/loremipsum.v1 v1.1.2
) )
require ( require (
github.com/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // 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/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -26,17 +27,20 @@ require (
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect go.mongodb.org/mongo-driver v1.12.1 // indirect
golang.org/x/crypto v0.9.0 // indirect golang.org/x/arch v0.5.0 // indirect
golang.org/x/net v0.10.0 // indirect golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sys v0.8.0 // indirect golang.org/x/net v0.17.0 // indirect
golang.org/x/text v0.9.0 // indirect golang.org/x/sys v0.13.0 // indirect
google.golang.org/protobuf v1.30.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 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.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 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-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-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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 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= 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/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 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.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 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.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 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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/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/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/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.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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.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 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 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 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.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.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.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 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= 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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 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 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
gogs.mikescher.com/BlackForestBytes/goext v0.0.155 h1:eFcWmq9OXk4yCw8mjuyz8+JWiUEqjeSGoWOHwO000co= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
gogs.mikescher.com/BlackForestBytes/goext v0.0.155/go.mod h1:bIcO9mw2CD03pmJcBaE5YCF6etG5u73OVkUa8Alx3D0= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
gogs.mikescher.com/BlackForestBytes/goext v0.0.156 h1:mRRyAkLQCGVbDfG1t+7Y3zTf1S4TNajENiRpX1KTGw8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
gogs.mikescher.com/BlackForestBytes/goext v0.0.156/go.mod h1:bIcO9mw2CD03pmJcBaE5YCF6etG5u73OVkUa8Alx3D0= 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.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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= 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.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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -9,6 +9,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"io" "io"
"net/http" "net/http"
"strings"
"time" "time"
) )
@@ -23,7 +24,9 @@ type AndroidPublisher struct {
func NewAndroidPublisherAPI(conf scn.Config) (AndroidPublisherClient, error) { func NewAndroidPublisherAPI(conf scn.Config) (AndroidPublisherClient, error) {
googauth, err := NewAuth(conf.GoogleAPITokenURI, conf.GoogleAPIPrivKeyID, conf.GoogleAPIClientMail, conf.GoogleAPIPrivateKey) pkey := strings.ReplaceAll(conf.GoogleAPIPrivateKey, "\\n", "\n")
googauth, err := NewAuth(conf.GoogleAPITokenURI, conf.GoogleAPIPrivKeyID, conf.GoogleAPIClientMail, pkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,11 +1,13 @@
package jobs package jobs
import ( import (
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"errors" "errors"
"fmt" "fmt"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/syncext" "gogs.mikescher.com/BlackForestBytes/goext/syncext"
"time" "time"
) )
@@ -131,7 +133,7 @@ func (j *DeliveryRetryJob) execute() (fastrr bool, err error) {
return len(deliveries) == 32, nil 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) client, err := j.app.Database.Primary.GetClient(ctx, delivery.ReceiverUserID, delivery.ReceiverClientID)
if err != nil { if err != nil {
@@ -156,7 +158,29 @@ func (j *DeliveryRetryJob) redeliver(ctx *logic.SimpleContext, delivery models.D
} }
} else { } else {
fcmDelivID, err := j.app.DeliverMessage(ctx, client, msg, nil) isCompatClient, err := j.app.Database.Primary.IsCompatClient(ctx, client.ClientID)
if err != nil {
log.Err(err).Str("MessageID", delivery.MessageID.String()).Str("ClientID", client.ClientID.String()).Msg("Failed to get <IsCompatClient>")
ctx.RollbackTransaction()
return
}
var titleOverride *string = nil
var msgidOverride *string = nil
if isCompatClient {
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()
return
}
titleOverride = langext.Ptr(j.app.CompatizeMessageTitle(ctx, msg))
msgidOverride = langext.Ptr(fmt.Sprintf("%d", messageIdComp))
}
fcmDelivID, err := j.app.DeliverMessage(ctx, client, msg, titleOverride, msgidOverride)
if err == nil { if err == nil {
err = j.app.Database.Primary.SetDeliverySuccess(ctx, delivery, fcmDelivID) err = j.app.Database.Primary.SetDeliverySuccess(ctx, delivery, fcmDelivID)
if err != nil { if err != nil {

View File

@@ -13,6 +13,15 @@ import (
"time" "time"
) )
type TxContext interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
GetOrCreateTransaction(db db.DatabaseImpl) (sq.Tx, error)
}
type AppContext struct { type AppContext struct {
app *Application app *Application
inner context.Context inner context.Context

View File

@@ -4,11 +4,13 @@ import (
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/google" "blackforestbytes.com/simplecloudnotifier/google"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"blackforestbytes.com/simplecloudnotifier/push" "blackforestbytes.com/simplecloudnotifier/push"
"context" "context"
"errors" "errors"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@@ -28,6 +30,7 @@ import (
var rexWhitespaceStart = rext.W(regexp.MustCompile("^\\s+")) var rexWhitespaceStart = rext.W(regexp.MustCompile("^\\s+"))
var rexWhitespaceEnd = rext.W(regexp.MustCompile("\\s+$")) var rexWhitespaceEnd = rext.W(regexp.MustCompile("\\s+$"))
var rexNormalizeUsername = rext.W(regexp.MustCompile("[^[:alnum:]\\-_ ]")) var rexNormalizeUsername = rext.W(regexp.MustCompile("[^[:alnum:]\\-_ ]"))
var rexCompatTitleChannel = rext.W(regexp.MustCompile("^\\[(?P<channel>[A-Za-z\\-0-9_ ]+)] (?P<title>(.|\\r|\\n)+)$"))
type Application struct { type Application struct {
Config scn.Config Config scn.Config
@@ -162,14 +165,6 @@ func (app *Application) GenerateRandomAuthKey() string {
return scn.RandomAuthKey() 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) { func (app *Application) VerifyProToken(ctx *AppContext, token string) (bool, error) {
if strings.HasPrefix(token, "ANDROID|v1|") { if strings.HasPrefix(token, "ANDROID|v1|") {
@@ -262,6 +257,10 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an
if err := g.ShouldBindWith(form, binding.Form); err != nil { if err := g.ShouldBindWith(form, binding.Form); err != nil {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form", err)) return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form", err))
} }
} else if g.ContentType() == "application/x-www-form-urlencoded" {
if err := g.ShouldBindWith(form, binding.Form); err != nil {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read urlencoded-form", err))
}
} else { } else {
if !ignoreWrongContentType { if !ignoreWrongContentType {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing form body", nil)) return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing form body", nil))
@@ -286,9 +285,9 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an
return actx, nil 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) 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) { func (app *Application) getPermissions(ctx *AppContext, hdr string) (models.PermissionSet, error) {
@@ -357,8 +356,8 @@ func (app *Application) NormalizeUsername(v string) string {
return strings.TrimSpace(v) return strings.TrimSpace(v)
} }
func (app *Application) DeliverMessage(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string) (string, error) { func (app *Application) DeliverMessage(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string, compatMsgIDOverride *string) (string, error) {
fcmDelivID, err := app.Pusher.SendNotification(ctx, client, msg, compatTitleOverride) fcmDelivID, err := app.Pusher.SendNotification(ctx, client, msg, compatTitleOverride, compatMsgIDOverride)
if err != nil { if err != nil {
log.Warn().Str("MessageID", msg.MessageID.String()).Str("ClientID", client.ClientID.String()).Err(err).Msg("FCM Delivery failed") log.Warn().Str("MessageID", msg.MessageID.String()).Str("ClientID", client.ClientID.String()).Err(err).Msg("FCM Delivery failed")
return "", err return "", err
@@ -372,3 +371,20 @@ func (app *Application) InsertRequestLog(data models.RequestLog) {
log.Error().Msg("failed to insert request-log (queue full)") log.Error().Msg("failed to insert request-log (queue full)")
} }
} }
func (app *Application) CompatizeMessageTitle(ctx TxContext, msg models.Message) string {
if msg.ChannelInternalName == "main" {
if rexCompatTitleChannel.IsMatch(msg.Title) {
return "!" + msg.Title // channel in title ?!
}
return msg.Title
}
channel, err := app.Database.Primary.GetChannelByID(ctx, msg.ChannelID)
if err != nil {
return fmt.Sprintf("[%s] %s", "%SCN-ERR%", msg.Title)
}
return fmt.Sprintf("[%s] %s", channel.DisplayName, msg.Title)
}

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/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
) )
@@ -43,7 +44,7 @@ func (ac *AppContext) CheckPermissionChanMessagesRead(channel models.Channel) *g
return nil // owned channel return nil // owned channel
} else { } else {
sub, err := ac.app.Database.Primary.GetSubscriptionBySubscriber(ac, p.Token.OwnerUserID, channel.ChannelID) 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)) return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
} }
if err != nil { if err != nil {
@@ -97,6 +98,15 @@ func (ac *AppContext) CheckPermissionMessageRead(msg models.Message) bool {
return false 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 { func (ac *AppContext) CheckPermissionAny() *ginresp.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.Token == nil { 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 package models
import "gogs.mikescher.com/BlackForestBytes/goext/langext" import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.com/BlackForestBytes/goext/enums"
const ChecksumGenerator = "4bfd61daa179e1452035a34c25c6f8170a08500bc0a7aa0e3981f95ad4b0d7d2" const ChecksumEnumGenerator = "8926e4a9845e67086109bef7ca447371ab5c0a94fcfd988f14fd4ee98da9e932" // GoExtVersion: 0.0.291
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"`
}
// ================================ ClientType ================================ // ================================ ClientType ================================
// //
@@ -58,11 +36,8 @@ func (e ClientType) ValuesAny() []any {
return langext.ArrCastToAny(__ClientTypeValues) return langext.ArrCastToAny(__ClientTypeValues)
} }
func (e ClientType) ValuesMeta() []EnumMetaValue { func (e ClientType) ValuesMeta() []enums.EnumMetaValue {
return []EnumMetaValue{ return ClientTypeValuesMeta()
EnumMetaValue{VarName: "ClientTypeAndroid", Value: ClientTypeAndroid, Description: nil},
EnumMetaValue{VarName: "ClientTypeIOS", Value: ClientTypeIOS, Description: nil},
}
} }
func (e ClientType) String() string { func (e ClientType) String() string {
@@ -76,6 +51,10 @@ func (e ClientType) VarName() string {
return "" return ""
} }
func (e ClientType) Meta() enums.EnumMetaValue {
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
}
func ParseClientType(vv string) (ClientType, bool) { func ParseClientType(vv string) (ClientType, bool) {
for _, ev := range __ClientTypeValues { for _, ev := range __ClientTypeValues {
if string(ev) == vv { if string(ev) == vv {
@@ -89,10 +68,10 @@ func ClientTypeValues() []ClientType {
return __ClientTypeValues return __ClientTypeValues
} }
func ClientTypeValuesMeta() []EnumMetaValue { func ClientTypeValuesMeta() []enums.EnumMetaValue {
return []EnumMetaValue{ return []enums.EnumMetaValue{
EnumMetaValue{VarName: "ClientTypeAndroid", Value: ClientTypeAndroid, Description: nil}, ClientTypeAndroid.Meta(),
EnumMetaValue{VarName: "ClientTypeIOS", Value: ClientTypeIOS, Description: nil}, ClientTypeIOS.Meta(),
} }
} }
@@ -127,12 +106,8 @@ func (e DeliveryStatus) ValuesAny() []any {
return langext.ArrCastToAny(__DeliveryStatusValues) return langext.ArrCastToAny(__DeliveryStatusValues)
} }
func (e DeliveryStatus) ValuesMeta() []EnumMetaValue { func (e DeliveryStatus) ValuesMeta() []enums.EnumMetaValue {
return []EnumMetaValue{ return DeliveryStatusValuesMeta()
EnumMetaValue{VarName: "DeliveryStatusRetry", Value: DeliveryStatusRetry, Description: nil},
EnumMetaValue{VarName: "DeliveryStatusSuccess", Value: DeliveryStatusSuccess, Description: nil},
EnumMetaValue{VarName: "DeliveryStatusFailed", Value: DeliveryStatusFailed, Description: nil},
}
} }
func (e DeliveryStatus) String() string { func (e DeliveryStatus) String() string {
@@ -146,6 +121,10 @@ func (e DeliveryStatus) VarName() string {
return "" return ""
} }
func (e DeliveryStatus) Meta() enums.EnumMetaValue {
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
}
func ParseDeliveryStatus(vv string) (DeliveryStatus, bool) { func ParseDeliveryStatus(vv string) (DeliveryStatus, bool) {
for _, ev := range __DeliveryStatusValues { for _, ev := range __DeliveryStatusValues {
if string(ev) == vv { if string(ev) == vv {
@@ -159,11 +138,11 @@ func DeliveryStatusValues() []DeliveryStatus {
return __DeliveryStatusValues return __DeliveryStatusValues
} }
func DeliveryStatusValuesMeta() []EnumMetaValue { func DeliveryStatusValuesMeta() []enums.EnumMetaValue {
return []EnumMetaValue{ return []enums.EnumMetaValue{
EnumMetaValue{VarName: "DeliveryStatusRetry", Value: DeliveryStatusRetry, Description: nil}, DeliveryStatusRetry.Meta(),
EnumMetaValue{VarName: "DeliveryStatusSuccess", Value: DeliveryStatusSuccess, Description: nil}, DeliveryStatusSuccess.Meta(),
EnumMetaValue{VarName: "DeliveryStatusFailed", Value: DeliveryStatusFailed, Description: nil}, DeliveryStatusFailed.Meta(),
} }
} }
@@ -207,13 +186,8 @@ func (e TokenPerm) ValuesAny() []any {
return langext.ArrCastToAny(__TokenPermValues) return langext.ArrCastToAny(__TokenPermValues)
} }
func (e TokenPerm) ValuesMeta() []EnumMetaValue { func (e TokenPerm) ValuesMeta() []enums.EnumMetaValue {
return []EnumMetaValue{ return TokenPermValuesMeta()
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) String() string { func (e TokenPerm) String() string {
@@ -234,6 +208,10 @@ func (e TokenPerm) VarName() string {
return "" 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) { func ParseTokenPerm(vv string) (TokenPerm, bool) {
for _, ev := range __TokenPermValues { for _, ev := range __TokenPermValues {
if string(ev) == vv { if string(ev) == vv {
@@ -247,11 +225,11 @@ func TokenPermValues() []TokenPerm {
return __TokenPermValues return __TokenPermValues
} }
func TokenPermValuesMeta() []EnumMetaValue { func TokenPermValuesMeta() []enums.EnumMetaValue {
return []EnumMetaValue{ return []enums.EnumMetaValue{
EnumMetaValue{VarName: "PermAdmin", Value: PermAdmin, Description: langext.Ptr("Edit userdata (+ includes all other permissions)")}, PermAdmin.Meta(),
EnumMetaValue{VarName: "PermChannelRead", Value: PermChannelRead, Description: langext.Ptr("Read messages")}, PermChannelRead.Meta(),
EnumMetaValue{VarName: "PermChannelSend", Value: PermChannelSend, Description: langext.Ptr("Send messages")}, PermChannelSend.Meta(),
EnumMetaValue{VarName: "PermUserRead", Value: PermUserRead, Description: langext.Ptr("Read userdata")}, PermUserRead.Meta(),
} }
} }

View File

@@ -1,19 +1,11 @@
package models package models
import ( 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" "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 { type EntityID interface {
String() string String() string
Valid() error Valid() error
@@ -23,389 +15,11 @@ type EntityID interface {
Regex() rext.Regex Regex() rext.Regex
} }
const idlen = 24 type UserID string //@csid:type [USR]
type ChannelID string //@csid:type [CHA]
const checklen = 1 type DeliveryID string //@csid:type [DEL]
type MessageID string //@csid:type [MSG]
const idCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" type SubscriptionID string //@csid:type [SUB]
const idCharsetLen = len(idCharset) type ClientID string //@csid:type [CLN]
type RequestID string //@csid:type [REQ]
var charSetReverseMap = generateCharsetMap() type KeyTokenID string //@csid:type [TOK]
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
}

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

@@ -129,6 +129,12 @@ type KeyTokenDB struct {
} }
func (k KeyTokenDB) Model() KeyToken { func (k KeyTokenDB) Model() KeyToken {
channels := make([]ChannelID, 0)
if strings.TrimSpace(k.Channels) != "" {
channels = langext.ArrMap(strings.Split(k.Channels, ";"), func(v string) ChannelID { return ChannelID(v) })
}
return KeyToken{ return KeyToken{
KeyTokenID: k.KeyTokenID, KeyTokenID: k.KeyTokenID,
Name: k.Name, Name: k.Name,
@@ -136,7 +142,7 @@ func (k KeyTokenDB) Model() KeyToken {
TimestampLastUsed: timeOptFromMilli(k.TimestampLastUsed), TimestampLastUsed: timeOptFromMilli(k.TimestampLastUsed),
OwnerUserID: k.OwnerUserID, OwnerUserID: k.OwnerUserID,
AllChannels: k.AllChannels, AllChannels: k.AllChannels,
Channels: langext.ArrMap(strings.Split(k.Channels, ";"), func(v string) ChannelID { return ChannelID(v) }), Channels: channels,
Token: k.Token, Token: k.Token,
Permissions: ParseTokenPermissionList(k.Permissions), Permissions: ParseTokenPermissionList(k.Permissions),
MessagesSent: k.MessagesSent, MessagesSent: k.MessagesSent,

View File

@@ -14,8 +14,7 @@ const (
type Message struct { type Message struct {
MessageID MessageID MessageID MessageID
SenderUserID UserID // user that sent the message SenderUserID UserID // user that sent the message (this is also the owner of the channel that contains it)
OwnerUserID UserID // oner of the message (= owner of the channel that contains it)
ChannelInternalName string ChannelInternalName string
ChannelID ChannelID ChannelID ChannelID
SenderName *string SenderName *string
@@ -34,7 +33,6 @@ func (m Message) FullJSON() MessageJSON {
return MessageJSON{ return MessageJSON{
MessageID: m.MessageID, MessageID: m.MessageID,
SenderUserID: m.SenderUserID, SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelInternalName: m.ChannelInternalName, ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID, ChannelID: m.ChannelID,
SenderName: m.SenderName, SenderName: m.SenderName,
@@ -53,7 +51,6 @@ func (m Message) TrimmedJSON() MessageJSON {
return MessageJSON{ return MessageJSON{
MessageID: m.MessageID, MessageID: m.MessageID,
SenderUserID: m.SenderUserID, SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelInternalName: m.ChannelInternalName, ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID, ChannelID: m.ChannelID,
SenderName: m.SenderName, SenderName: m.SenderName,
@@ -99,7 +96,6 @@ func (m Message) ShortContent() string {
type MessageJSON struct { type MessageJSON struct {
MessageID MessageID `json:"message_id"` MessageID MessageID `json:"message_id"`
SenderUserID UserID `json:"sender_user_id"` SenderUserID UserID `json:"sender_user_id"`
OwnerUserID UserID `json:"owner_user_id"`
ChannelInternalName string `json:"channel_internal_name"` ChannelInternalName string `json:"channel_internal_name"`
ChannelID ChannelID `json:"channel_id"` ChannelID ChannelID `json:"channel_id"`
SenderName *string `json:"sender_name"` SenderName *string `json:"sender_name"`
@@ -116,7 +112,6 @@ type MessageJSON struct {
type MessageDB struct { type MessageDB struct {
MessageID MessageID `db:"message_id"` MessageID MessageID `db:"message_id"`
SenderUserID UserID `db:"sender_user_id"` SenderUserID UserID `db:"sender_user_id"`
OwnerUserID UserID `db:"owner_user_id"`
ChannelInternalName string `db:"channel_internal_name"` ChannelInternalName string `db:"channel_internal_name"`
ChannelID ChannelID `db:"channel_id"` ChannelID ChannelID `db:"channel_id"`
SenderName *string `db:"sender_name"` SenderName *string `db:"sender_name"`
@@ -135,7 +130,6 @@ func (m MessageDB) Model() Message {
return Message{ return Message{
MessageID: m.MessageID, MessageID: m.MessageID,
SenderUserID: m.SenderUserID, SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelInternalName: m.ChannelInternalName, ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID, ChannelID: m.ChannelID,
SenderName: m.SenderName, SenderName: m.SenderName,

View File

@@ -17,7 +17,6 @@ type MessageFilter struct {
ConfirmedSubscriptionBy *UserID ConfirmedSubscriptionBy *UserID
SearchString *[]string SearchString *[]string
Sender *[]UserID Sender *[]UserID
Owner *[]UserID
ChannelNameCS *[]string // case-sensitive ChannelNameCS *[]string // case-sensitive
ChannelNameCI *[]string // case-insensitive ChannelNameCI *[]string // case-insensitive
ChannelID *[]ChannelID ChannelID *[]ChannelID
@@ -79,15 +78,6 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") 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 { if f.ChannelNameCI != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.ChannelNameCI { for i, v := range *f.ChannelNameCI {
@@ -169,7 +159,7 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
if f.TimestampRealBefore != nil { if f.TimestampRealBefore != nil {
sqlClauses = append(sqlClauses, "(timestamp_real < :ts_real_before)") 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 { 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

@@ -23,17 +23,24 @@ type User struct {
func (u User) JSON() UserJSON { func (u User) JSON() UserJSON {
return UserJSON{ return UserJSON{
UserID: u.UserID, UserID: u.UserID,
Username: u.Username, Username: u.Username,
TimestampCreated: u.TimestampCreated.Format(time.RFC3339Nano), TimestampCreated: u.TimestampCreated.Format(time.RFC3339Nano),
TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano), TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano),
TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano), TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano),
MessagesSent: u.MessagesSent, MessagesSent: u.MessagesSent,
QuotaUsed: u.QuotaUsedToday(), QuotaUsed: u.QuotaUsedToday(),
QuotaPerDay: u.QuotaPerDay(), QuotaPerDay: u.QuotaPerDay(),
QuotaRemaining: u.QuotaRemainingToday(), QuotaRemaining: u.QuotaRemainingToday(),
IsPro: u.IsPro, IsPro: u.IsPro,
DefaultChannel: u.DefaultChannel(), 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 { func (u User) MaxContentLength() int {
if u.IsPro { if u.IsPro {
return 16384 return 2 * 1024 * 1024 // 2 MB
} else { } else {
return 2048 return 2 * 1024 // 2 KB
} }
} }
@@ -61,7 +68,7 @@ func (u User) MaxTitleLength() int {
func (u User) QuotaPerDay() int { func (u User) QuotaPerDay() int {
if u.IsPro { if u.IsPro {
return 1000 return 5000
} else { } else {
return 50 return 50
} }
@@ -92,15 +99,15 @@ func (u User) MaxChannelNameLength() int {
return 120 return 120
} }
func (u User) MaxChannelDescriptionNameLength() int { func (u User) MaxChannelDescriptionLength() int {
return 300 return 300
} }
func (u User) MaxSenderName() int { func (u User) MaxSenderNameLength() int {
return 120 return 120
} }
func (u User) MaxUserMessageID() int { func (u User) MaxUserMessageIDLength() int {
return 64 return 64
} }
@@ -109,17 +116,24 @@ func (u User) MaxTimestampDiffHours() int {
} }
type UserJSON struct { type UserJSON struct {
UserID UserID `json:"user_id"` UserID UserID `json:"user_id"`
Username *string `json:"username"` Username *string `json:"username"`
TimestampCreated string `json:"timestamp_created"` TimestampCreated string `json:"timestamp_created"`
TimestampLastRead *string `json:"timestamp_lastread"` TimestampLastRead *string `json:"timestamp_lastread"`
TimestampLastSent *string `json:"timestamp_lastsent"` TimestampLastSent *string `json:"timestamp_lastsent"`
MessagesSent int `json:"messages_sent"` MessagesSent int `json:"messages_sent"`
QuotaUsed int `json:"quota_used"` QuotaUsed int `json:"quota_used"`
QuotaRemaining int `json:"quota_remaining"` QuotaRemaining int `json:"quota_remaining"`
QuotaPerDay int `json:"quota_max"` QuotaPerDay int `json:"quota_max"`
IsPro bool `json:"is_pro"` IsPro bool `json:"is_pro"`
DefaultChannel string `json:"default_channel"` 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 { type UserJSONWithClientsAndKeys struct {

View File

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

View File

@@ -12,6 +12,6 @@ func NewDummy() NotificationClient {
return &DummyConnector{} return &DummyConnector{}
} }
func (d DummyConnector) SendNotification(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string) (string, error) { func (d DummyConnector) SendNotification(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string, compatMsgIDOverride *string) (string, error) {
return "%DUMMY%", nil return "%DUMMY%", nil
} }

View File

@@ -15,6 +15,7 @@ import (
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -29,7 +30,9 @@ type FirebaseConnector struct {
func NewFirebaseConn(conf scn.Config) (NotificationClient, error) { func NewFirebaseConn(conf scn.Config) (NotificationClient, error) {
fbauth, err := NewAuth(conf.FirebaseTokenURI, conf.FirebaseProjectID, conf.FirebaseClientMail, conf.FirebasePrivateKey) pkey := strings.ReplaceAll(conf.FirebasePrivateKey, "\\n", "\n")
fbauth, err := NewAuth(conf.FirebaseTokenURI, conf.FirebaseProjectID, conf.FirebaseClientMail, pkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -50,13 +53,13 @@ type Notification struct {
Priority int Priority int
} }
func (fb FirebaseConnector) SendNotification(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string) (string, error) { func (fb FirebaseConnector) SendNotification(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string, compatMsgIDOverride *string) (string, error) {
uri := "https://fcm.googleapis.com/v1/projects/" + fb.fbProject + "/messages:send" uri := "https://fcm.googleapis.com/v1/projects/" + fb.fbProject + "/messages:send"
jsonBody := gin.H{ jsonBody := gin.H{
"data": gin.H{ "data": gin.H{
"scn_msg_id": msg.MessageID.String(), "scn_msg_id": langext.Coalesce(compatMsgIDOverride, msg.MessageID.String()),
"usr_msg_id": langext.Coalesce(msg.UserMessageID, ""), "usr_msg_id": langext.Coalesce(msg.UserMessageID, ""),
"client_id": client.ClientID.String(), "client_id": client.ClientID.String(),
"timestamp": strconv.FormatInt(msg.Timestamp().Unix(), 10), "timestamp": strconv.FormatInt(msg.Timestamp().Unix(), 10),
@@ -124,5 +127,7 @@ func (fb FirebaseConnector) SendNotification(ctx context.Context, client models.
return "", err return "", err
} }
log.Info().Msg(fmt.Sprintf("Sucessfully pushed notification %s", msg.MessageID))
return respBody.Name, nil return respBody.Name, nil
} }

View File

@@ -6,5 +6,5 @@ import (
) )
type NotificationClient interface { type NotificationClient interface {
SendNotification(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string) (string, error) SendNotification(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string, compatMsgIDOverride *string) (string, error)
} }

View File

@@ -11,6 +11,7 @@ type SinkData struct {
Message models.Message Message models.Message
Client models.Client Client models.Client
CompatTitleOverride *string CompatTitleOverride *string
CompatMsgIDOverride *string
} }
type TestSink struct { type TestSink struct {
@@ -25,7 +26,7 @@ func (d *TestSink) Last() SinkData {
return d.Data[len(d.Data)-1] return d.Data[len(d.Data)-1]
} }
func (d *TestSink) SendNotification(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string) (string, error) { func (d *TestSink) SendNotification(ctx context.Context, client models.Client, msg models.Message, compatTitleOverride *string, compatMsgIDOverride *string) (string, error) {
id, err := langext.NewHexUUID() id, err := langext.NewHexUUID()
if err != nil { if err != nil {
return "", err return "", err
@@ -37,6 +38,7 @@ func (d *TestSink) SendNotification(ctx context.Context, client models.Client, m
Message: msg, Message: msg,
Client: client, Client: client,
CompatTitleOverride: compatTitleOverride, CompatTitleOverride: compatTitleOverride,
CompatMsgIDOverride: compatMsgIDOverride,
}) })
return key, nil return key, nil

View File

@@ -6,7 +6,7 @@
"contact": {}, "contact": {},
"version": "2.0" "version": "2.0"
}, },
"host": "scn.blackforestbytes.com", "host": "simplecloudnotifier.de",
"basePath": "/", "basePath": "/",
"paths": { "paths": {
"/": { "/": {
@@ -958,13 +958,13 @@
} }
}, },
"/api/v2/messages/{mid}": { "/api/v2/messages/{mid}": {
"delete": { "get": {
"description": "The user must own the message and request the resource with the ADMIN Key", "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": [ "tags": [
"API-v2" "API-v2"
], ],
"summary": "Delete a single message", "summary": "Get a single message (untrimmed)",
"operationId": "api-messages-delete", "operationId": "api-messages-get",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
@@ -1007,13 +1007,13 @@
} }
} }
}, },
"patch": { "delete": {
"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", "description": "The user must own the message and request the resource with the ADMIN Key",
"tags": [ "tags": [
"API-v2" "API-v2"
], ],
"summary": "Get a single message (untrimmed)", "summary": "Delete a single message",
"operationId": "api-messages-get", "operationId": "api-messages-delete",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
@@ -2160,7 +2160,7 @@
}, },
"/api/v2/users/{uid}/subscriptions": { "/api/v2/users/{uid}/subscriptions": {
"get": { "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": [ "tags": [
"API-v2" "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": { "/send": {
"post": { "post": {
"description": "All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required", "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": [ "parameters": [
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "query" "in": "query"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "query" "in": "query"
}, },
{ {
"type": "integer", "type": "string",
"example": "7725",
"name": "user_id", "name": "user_id",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"name": "user_key", "example": "test",
"in": "query" "name": "channel",
"in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "formData" "in": "formData"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "formData" "in": "formData"
}, },
{
"type": "integer",
"name": "user_id",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"name": "user_key", "example": "7725",
"name": "user_id",
"in": "formData" "in": "formData"
} }
], ],
@@ -2702,7 +2848,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handler.SendMessageCompat.response" "$ref": "#/definitions/handler.SendMessage.response"
} }
}, },
"400": { "400": {
@@ -2952,8 +3098,6 @@
"handler.CreateUserKey.body": { "handler.CreateUserKey.body": {
"type": "object", "type": "object",
"required": [ "required": [
"all_channels",
"channels",
"name", "name",
"permissions" "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": { "handler.Sleep.response": {
"type": "object", "type": "object",
"properties": { "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": { "handler.pingResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -3574,9 +3733,6 @@
"message_id": { "message_id": {
"type": "string" "type": "string"
}, },
"owner_user_id": {
"type": "string"
},
"priority": { "priority": {
"type": "integer" "type": "integer"
}, },
@@ -3638,9 +3794,30 @@
"default_channel": { "default_channel": {
"type": "string" "type": "string"
}, },
"default_priority": {
"type": "integer"
},
"is_pro": { "is_pro": {
"type": "boolean" "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": { "messages_sent": {
"type": "integer" "type": "integer"
}, },
@@ -3685,9 +3862,30 @@
"default_channel": { "default_channel": {
"type": "string" "type": "string"
}, },
"default_priority": {
"type": "integer"
},
"is_pro": { "is_pro": {
"type": "boolean" "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": { "messages_sent": {
"type": "integer" "type": "integer"
}, },

View File

@@ -183,8 +183,6 @@ definitions:
permissions: permissions:
type: string type: string
required: required:
- all_channels
- channels
- name - name
- permissions - permissions
type: object type: object
@@ -378,29 +376,6 @@ definitions:
suppress_send: suppress_send:
type: boolean type: boolean
type: object 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: handler.Sleep.response:
properties: properties:
duration: duration:
@@ -460,6 +435,38 @@ definitions:
user_id: user_id:
type: integer type: integer
type: object 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: handler.pingResponse:
properties: properties:
info: info:
@@ -583,8 +590,6 @@ definitions:
type: string type: string
message_id: message_id:
type: string type: string
owner_user_id:
type: string
priority: priority:
type: integer type: integer
sender_ip: sender_ip:
@@ -625,8 +630,22 @@ definitions:
properties: properties:
default_channel: default_channel:
type: string type: string
default_priority:
type: integer
is_pro: is_pro:
type: boolean 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: messages_sent:
type: integer type: integer
quota_max: quota_max:
@@ -656,8 +675,22 @@ definitions:
type: array type: array
default_channel: default_channel:
type: string type: string
default_priority:
type: integer
is_pro: is_pro:
type: boolean 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: messages_sent:
type: integer type: integer
quota_max: quota_max:
@@ -681,7 +714,7 @@ definitions:
username: username:
type: string type: string
type: object type: object
host: scn.blackforestbytes.com host: simplecloudnotifier.de
info: info:
contact: {} contact: {}
description: API for SCN description: API for SCN
@@ -1369,7 +1402,7 @@ paths:
summary: Delete a single message summary: Delete a single message
tags: tags:
- API-v2 - API-v2
patch: get:
description: |- description: |-
The user must either own the message and request the resource with the READ or ADMIN Key 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 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: /api/v2/users/{uid}/subscriptions:
get: get:
description: |- description: |-
The possible values for 'selector' are: The possible values for 'direction' are:
- "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels) - "outgoing" Subscriptions with the user as subscriber (= subscriptions he can use to read channels)
- "outgoing_confirmed" Confirmed subscriptions with the user as subscriber - "incoming" Subscriptions to channels of this user (= incoming subscriptions and subscription requests)
- "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber - "both" Combines "outgoing" and "incoming" (default)
- "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 The possible values for 'confirmation' are:
- "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests) - "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 operationId: api-user-subscriptions-list
parameters: parameters:
- description: UserID - description: UserID
@@ -2354,6 +2398,70 @@ paths:
summary: Update a subscription (e.g. confirm) summary: Update a subscription (e.g. confirm)
tags: tags:
- API-v2 - 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: /send:
post: post:
description: All parameter can be set via query-parameter or the json body. 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. description: All parameter can be set via query-parameter or form-data body.
Only UserID, UserKey and Title are required Only UserID, UserKey and Title are required
parameters: parameters:
- in: query - example: test
in: query
name: channel
type: string
- example: This is a message
in: query
name: content name: content
type: string type: string
- in: query - example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id name: msg_id
type: string type: string
- in: query - enum:
- 0
- 1
- 2
example: 1
in: query
name: priority name: priority
type: integer type: integer
- in: query - example: example-server
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp name: timestamp
type: number type: number
- in: query - example: Hello World
in: query
name: title name: title
type: string type: string
- in: query - example: "7725"
in: query
name: user_id name: user_id
type: integer
- in: query
name: user_key
type: string type: string
- in: formData - example: test
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content name: content
type: string type: string
- in: formData - example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id name: msg_id
type: string type: string
- in: formData - enum:
- 0
- 1
- 2
example: 1
in: formData
name: priority name: priority
type: integer type: integer
- in: formData - example: example-server
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp name: timestamp
type: number type: number
- in: formData - example: Hello World
in: formData
name: title name: title
type: string type: string
- in: formData - example: "7725"
in: formData
name: user_id name: user_id
type: integer
- in: formData
name: user_key
type: string type: string
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handler.SendMessageCompat.response' $ref: '#/definitions/handler.SendMessage.response'
"400": "400":
description: Bad Request description: Bad Request
schema: 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) { 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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,
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 { 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{}) 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, "success", true, r0["success"])
tt.AssertEqual(t, "message", "New user registered", r0["message"]) tt.AssertEqual(t, "message", "New user registered", r0["message"])
tt.AssertEqual(t, "quota", 0, r0["quota"]) 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"]) 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"))) 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")))
@@ -364,7 +364,7 @@ func TestCompatInfo(t *testing.T) {
tt.AssertEqual(t, "message", "ok", r2["message"]) tt.AssertEqual(t, "message", "ok", r2["message"])
tt.AssertEqual(t, "quota", 1, r2["quota"]) tt.AssertEqual(t, "quota", 1, r2["quota"])
tt.AssertEqual(t, "quota_max", 50, r2["quota_max"]) tt.AssertEqual(t, "quota_max", 50, r2["quota_max"])
tt.AssertEqual(t, "unack_count", 0, r2["unack_count"]) tt.AssertEqual(t, "unack_count", 1, r2["unack_count"])
tt.AssertEqual(t, "user_id", userid, r2["user_id"]) tt.AssertEqual(t, "user_id", userid, r2["user_id"])
tt.AssertEqual(t, "user_key", userkey, r2["user_key"]) tt.AssertEqual(t, "user_key", userkey, r2["user_key"])
@@ -491,7 +491,7 @@ func TestCompatUpdateUserKey(t *testing.T) {
tt.AssertEqual(t, "message", "ok", r1["message"]) tt.AssertEqual(t, "message", "ok", r1["message"])
tt.AssertEqual(t, "quota", 1, r1["quota"]) tt.AssertEqual(t, "quota", 1, r1["quota"])
tt.AssertEqual(t, "quota_max", 50, r1["quota_max"]) tt.AssertEqual(t, "quota_max", 50, r1["quota_max"])
tt.AssertEqual(t, "unack_count", 0, r1["unack_count"]) tt.AssertEqual(t, "unack_count", 1, r1["unack_count"])
tt.AssertEqual(t, "user_id", userid, r1["user_id"]) tt.AssertEqual(t, "user_id", userid, r1["user_id"])
tt.AssertEqual(t, "user_key", newkey, r1["user_key"]) tt.AssertEqual(t, "user_key", newkey, r1["user_key"])
@@ -528,7 +528,7 @@ func TestCompatUpdateFCM(t *testing.T) {
tt.AssertEqual(t, "message", "ok", r1["message"]) tt.AssertEqual(t, "message", "ok", r1["message"])
tt.AssertEqual(t, "quota", 1, r1["quota"]) tt.AssertEqual(t, "quota", 1, r1["quota"])
tt.AssertEqual(t, "quota_max", 50, r1["quota_max"]) tt.AssertEqual(t, "quota_max", 50, r1["quota_max"])
tt.AssertEqual(t, "unack_count", 0, r1["unack_count"]) tt.AssertEqual(t, "unack_count", 1, r1["unack_count"])
tt.AssertEqual(t, "user_id", userid, r1["user_id"]) tt.AssertEqual(t, "user_id", userid, r1["user_id"])
tt.AssertEqual(t, "user_key", newkey, r1["user_key"]) tt.AssertEqual(t, "user_key", newkey, r1["user_key"])
@@ -555,7 +555,7 @@ func TestCompatUpgrade(t *testing.T) {
tt.AssertEqual(t, "success", true, r1["success"]) tt.AssertEqual(t, "success", true, r1["success"])
tt.AssertEqual(t, "message", "user updated", r1["message"]) tt.AssertEqual(t, "message", "user updated", r1["message"])
tt.AssertEqual(t, "quota", 0, r1["quota"]) 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"]) tt.AssertEqual(t, "is_pro", true, r1["is_pro"])
} }
@@ -732,3 +732,90 @@ func TestCompatTitlePatch(t *testing.T) {
tt.AssertStrRepEqual(t, "msg.ovrTitle", "[TestChan] HelloWorld_001", pusher.Last().CompatTitleOverride) tt.AssertStrRepEqual(t, "msg.ovrTitle", "[TestChan] HelloWorld_001", pusher.Last().CompatTitleOverride)
} }
func TestCompatAckCount(t *testing.T) {
_, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
r0 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/register.php?fcm_token=%s&pro=%s&pro_token=%s", "DUMMY_FCM", "0", ""))
tt.AssertEqual(t, "success", true, r0["success"])
userid := int64(r0["user_id"].(float64))
userkey := r0["user_key"].(string)
{
ri1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/info.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "unack_count", 0, ri1["unack_count"])
}
r1 := tt.RequestPost[gin.H](t, baseUrl, "/send.php", tt.FormData{
"user_id": fmt.Sprintf("%d", userid),
"user_key": userkey,
"title": "my title 11 & x",
})
tt.AssertEqual(t, "success", true, r1["success"])
r1scnid := int64(r1["scn_msg_id"].(float64))
{
ri1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/info.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "unack_count", 1, ri1["unack_count"])
}
r2 := tt.RequestPost[gin.H](t, baseUrl, "/send.php", tt.FormData{
"user_id": fmt.Sprintf("%d", userid),
"user_key": userkey,
"title": "my title 11 & x",
})
tt.AssertEqual(t, "success", true, r2["success"])
r2scnid := int64(r2["scn_msg_id"].(float64))
{
ri1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/info.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "unack_count", 2, ri1["unack_count"])
}
{
ack := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, r1scnid))
tt.AssertEqual(t, "success", true, ack["success"])
tt.AssertEqual(t, "prev_ack", 0, ack["prev_ack"])
tt.AssertEqual(t, "new_ack", 1, ack["new_ack"])
tt.AssertEqual(t, "message", "ok", ack["message"])
}
{
ri1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/info.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "unack_count", 1, ri1["unack_count"])
}
{
ack := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, r1scnid))
tt.AssertEqual(t, "success", true, ack["success"])
tt.AssertEqual(t, "prev_ack", 1, ack["prev_ack"])
tt.AssertEqual(t, "new_ack", 1, ack["new_ack"])
tt.AssertEqual(t, "message", "ok", ack["message"])
}
{
ri1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/info.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "unack_count", 1, ri1["unack_count"])
}
{
ri1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/info.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "unack_count", 1, ri1["unack_count"])
}
{
ack := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/ack.php?user_id=%d&user_key=%s&scn_msg_id=%d", userid, userkey, r2scnid))
tt.AssertEqual(t, "success", true, ack["success"])
tt.AssertEqual(t, "prev_ack", 0, ack["prev_ack"])
tt.AssertEqual(t, "new_ack", 1, ack["new_ack"])
tt.AssertEqual(t, "message", "ok", ack["message"])
}
{
ri1 := tt.RequestGet[gin.H](t, baseUrl, fmt.Sprintf("/api/info.php?user_id=%d&user_key=%s", userid, userkey))
tt.AssertEqual(t, "unack_count", 0, ri1["unack_count"])
}
}

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

@@ -383,6 +383,11 @@ func TestTokenKeysDowngradeSelf(t *testing.T) {
data := tt.InitSingleData(t, ws) data := tt.InitSingleData(t, ws)
chan0 := tt.RequestAuthPost[gin.H](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data.UID), gin.H{
"name": "testchan1",
})
chanid := fmt.Sprintf("%v", chan0["channel_id"])
type keyobj struct { type keyobj struct {
AllChannels bool `json:"all_channels"` AllChannels bool `json:"all_channels"`
Channels []string `json:"channels"` Channels []string `json:"channels"`
@@ -415,7 +420,7 @@ func TestTokenKeysDowngradeSelf(t *testing.T) {
}, 400, apierr.CANNOT_SELFUPDATE_KEY) }, 400, apierr.CANNOT_SELFUPDATE_KEY)
tt.RequestAuthPatchShouldFail(t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys/%s", data.UID, ak), gin.H{ tt.RequestAuthPatchShouldFail(t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys/%s", data.UID, ak), gin.H{
"channels": []string{"main"}, "channels": []string{chanid},
}, 400, apierr.CANNOT_SELFUPDATE_KEY) }, 400, apierr.CANNOT_SELFUPDATE_KEY)
tt.RequestAuthPatch[tt.Void](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys/%s", data.UID, ak), gin.H{ tt.RequestAuthPatch[tt.Void](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys/%s", data.UID, ak), gin.H{
@@ -606,3 +611,64 @@ func TestTokenKeysMessageCounter(t *testing.T) {
assertCounter(4, 4, 0) assertCounter(4, 4, 0)
} }
func TestTokenKeysCreateDefaultParam(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitSingleData(t, ws)
type keyobj struct {
AllChannels bool `json:"all_channels"`
Channels []string `json:"channels"`
KeytokenId string `json:"keytoken_id"`
MessagesSent int `json:"messages_sent"`
Name string `json:"name"`
OwnerUserId string `json:"owner_user_id"`
Permissions string `json:"permissions"`
Token string `json:"token"` // only in create
}
chan0 := tt.RequestAuthPost[gin.H](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data.UID), gin.H{
"name": "testchan1",
})
chanid := fmt.Sprintf("%v", chan0["channel_id"])
{
key2 := tt.RequestAuthPost[keyobj](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys", data.UID), gin.H{
"name": "K2",
"permissions": "CS",
})
tt.AssertEqual(t, "Name", "K2", key2.Name)
tt.AssertEqual(t, "Permissions", "CS", key2.Permissions)
tt.AssertEqual(t, "AllChannels", true, key2.AllChannels)
tt.AssertEqual(t, "Channels.Len", 0, len(key2.Channels))
}
{
key2 := tt.RequestAuthPost[keyobj](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys", data.UID), gin.H{
"name": "K3",
"permissions": "CS",
"channels": []string{chanid},
})
tt.AssertEqual(t, "Name", "K3", key2.Name)
tt.AssertEqual(t, "Permissions", "CS", key2.Permissions)
tt.AssertEqual(t, "AllChannels", false, key2.AllChannels)
tt.AssertEqual(t, "Channels.Len", 1, len(key2.Channels))
}
{
key2 := tt.RequestAuthPost[keyobj](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys", data.UID), gin.H{
"name": "K4",
"permissions": "CS",
"all_channels": false,
})
tt.AssertEqual(t, "Name", "K4", key2.Name)
tt.AssertEqual(t, "Permissions", "CS", key2.Permissions)
tt.AssertEqual(t, "AllChannels", false, key2.AllChannels)
tt.AssertEqual(t, "Channels.Len", 0, len(key2.Channels))
}
}

View File

@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/url" "net/url"
"strings"
"testing" "testing"
"time" "time"
) )
@@ -438,10 +439,13 @@ func TestSendLongContentPro(t *testing.T) {
uid := r0["user_id"].(string) uid := r0["user_id"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
str1k := strings.Repeat(".", 1000)
str100k := strings.Repeat(":", 100_000)
{ {
longContent := "" longContent := ""
for i := 0; i < 400; i++ { 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{ tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
@@ -455,7 +459,7 @@ func TestSendLongContentPro(t *testing.T) {
{ {
longContent := "" longContent := ""
for i := 0; i < 800; i++ { 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{ tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
@@ -469,8 +473,8 @@ func TestSendLongContentPro(t *testing.T) {
{ {
longContent := "" longContent := ""
for i := 0; i < 1600; i++ { for i := 0; i < 16; i++ {
longContent += "123456789\n" // 10 * 1600 = 16_000 (max = 16_384) longContent += str1k // 16 * 1000 = 16_000 (max = 2_097_152)
} }
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{ tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
@@ -483,22 +487,8 @@ func TestSendLongContentPro(t *testing.T) {
{ {
longContent := "" longContent := ""
for i := 0; i < 1630; i++ { for i := 0; i < 21; i++ {
longContent += "123456789\n" // 10 * 1630 = 163_000 (max = 16_384) longContent += str100k // 21 * 200_000 = 2_100_000 (max = 2_097_152)
}
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)
} }
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
@@ -1276,8 +1266,8 @@ func TestQuotaExceededPro(t *testing.T) {
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
tt.AssertStrRepEqual(t, "quota.0", 0, r0["quota_used"]) tt.AssertStrRepEqual(t, "quota.0", 0, r0["quota_used"])
tt.AssertStrRepEqual(t, "quota.0", 1000, r0["quota_max"]) tt.AssertStrRepEqual(t, "quota.0", 5000, r0["quota_max"])
tt.AssertStrRepEqual(t, "quota.0", 1000, r0["quota_remaining"]) tt.AssertStrRepEqual(t, "quota.0", 5000, r0["quota_remaining"])
{ {
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@@ -1286,18 +1276,18 @@ func TestQuotaExceededPro(t *testing.T) {
"title": tt.ShortLipsum0(2), "title": tt.ShortLipsum0(2),
}) })
tt.AssertStrRepEqual(t, "quota.msg.1", 1, msg1["quota"]) 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)) 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", 1, usr["quota_used"])
tt.AssertStrRepEqual(t, "quota.1", 1000, usr["quota_max"]) tt.AssertStrRepEqual(t, "quota.1", 5000, usr["quota_max"])
tt.AssertStrRepEqual(t, "quota.1", 999, usr["quota_remaining"]) 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{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "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)) 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", 4999, usr["quota_used"])
tt.AssertStrRepEqual(t, "quota.999", 1000, usr["quota_max"]) tt.AssertStrRepEqual(t, "quota.999", 5000, usr["quota_max"])
tt.AssertStrRepEqual(t, "quota.999", 1, usr["quota_remaining"]) tt.AssertStrRepEqual(t, "quota.999", 1, usr["quota_remaining"])
} }
@@ -1319,15 +1309,15 @@ func TestQuotaExceededPro(t *testing.T) {
"user_id": uid, "user_id": uid,
"title": tt.ShortLipsum0(2), "title": tt.ShortLipsum0(2),
}) })
tt.AssertStrRepEqual(t, "quota.msg.1000", 1000, msg50["quota"]) tt.AssertStrRepEqual(t, "quota.msg.5000", 5000, msg50["quota"])
tt.AssertStrRepEqual(t, "quota.msg.1000", 1000, msg50["quota_max"]) 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)) 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.5000", 5000, usr["quota_used"])
tt.AssertStrRepEqual(t, "quota.1000", 1000, usr["quota_max"]) tt.AssertStrRepEqual(t, "quota.5000", 5000, usr["quota_max"])
tt.AssertStrRepEqual(t, "quota.1000", 0, usr["quota_remaining"]) tt.AssertStrRepEqual(t, "quota.5000", 0, usr["quota_remaining"])
} }
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{

View File

@@ -51,17 +51,36 @@ func TestListSubscriptionsOfUser(t *testing.T) {
Channels []chanobj `json:"channels"` Channels []chanobj `json:"channels"`
} }
assertCount := func(u tt.Userdat, c int, sel string) { 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?selector=%s", u.UID, sel)) 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, sel+".len", c, len(slist.Subscriptions)) tt.AssertEqual(t, dir+"."+conf+".len", c, len(slist.Subscriptions))
} }
assertCount(data.User[16], 3, "outgoing_all") assertCount2 := func(u tt.Userdat, c int, dir string, conf string, ext string) {
assertCount(data.User[16], 3, "outgoing_confirmed") 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))
assertCount(data.User[16], 0, "outgoing_unconfirmed") tt.AssertEqual(t, dir+"."+conf+"."+ext+".len", c, len(slist.Subscriptions))
assertCount(data.User[16], 3, "incoming_all") }
assertCount(data.User[16], 3, "incoming_confirmed")
assertCount(data.User[16], 0, "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], 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)) 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" }) 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{}) 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", "all")
assertCount(data.User[16], 3, "outgoing_confirmed") assertCount(data.User[16], 3, "outgoing", "confirmed")
assertCount(data.User[16], 0, "outgoing_unconfirmed") assertCount(data.User[16], 0, "outgoing", "unconfirmed")
assertCount(data.User[16], 5, "incoming_all") assertCount(data.User[16], 5, "incoming", "all")
assertCount(data.User[16], 4, "incoming_confirmed") assertCount(data.User[16], 4, "incoming", "confirmed")
assertCount(data.User[16], 1, "incoming_unconfirmed") 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{ 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, "confirmed": false,
}) })
assertCount(data.User[16], 5, "incoming_all") assertCount(data.User[16], 5, "incoming", "all")
assertCount(data.User[16], 3, "incoming_confirmed") assertCount(data.User[16], 3, "incoming", "confirmed")
assertCount(data.User[16], 2, "incoming_unconfirmed") assertCount(data.User[16], 2, "incoming", "unconfirmed")
assertCount(data.User[0], 7, "outgoing_all") assertCount2(data.User[16], 2, "incoming", "all", "true")
assertCount(data.User[0], 5, "outgoing_confirmed") assertCount2(data.User[16], 0, "incoming", "confirmed", "true")
assertCount(data.User[0], 2, "outgoing_unconfirmed") assertCount2(data.User[16], 2, "incoming", "unconfirmed", "true")
assertCount(data.User[0], 5, "incoming_all")
assertCount(data.User[0], 5, "incoming_confirmed") assertCount2(data.User[16], 3, "incoming", "all", "false")
assertCount(data.User[0], 0, "incoming_unconfirmed") 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) { func TestListSubscriptionsOfChannel(t *testing.T) {
@@ -537,9 +592,15 @@ func TestGetSubscriptionToForeignChannel(t *testing.T) {
Channels []chanobj `json:"channels"` Channels []chanobj `json:"channels"`
} }
assertCount := func(u tt.Userdat, c int, sel string) { 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?selector=%s", u.UID, sel)) 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, sel+".len", c, len(slist.Subscriptions)) 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)) 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{}) 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", "all")
assertCount(data.User[16], 3, "outgoing_confirmed") assertCount(data.User[16], 3, "outgoing", "confirmed")
assertCount(data.User[16], 0, "outgoing_unconfirmed") assertCount(data.User[16], 0, "outgoing", "unconfirmed")
assertCount(data.User[16], 5, "incoming_all") assertCount(data.User[16], 5, "incoming", "all")
assertCount(data.User[16], 4, "incoming_confirmed") assertCount(data.User[16], 4, "incoming", "confirmed")
assertCount(data.User[16], 1, "incoming_unconfirmed") assertCount(data.User[16], 1, "incoming", "unconfirmed")
assertCount(data.User[0], 7, "outgoing_all") assertCount2(data.User[16], 0, "outgoing", "all", "true")
assertCount(data.User[0], 6, "outgoing_confirmed") assertCount2(data.User[16], 0, "outgoing", "confirmed", "true")
assertCount(data.User[0], 1, "outgoing_unconfirmed") assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "true")
assertCount(data.User[0], 5, "incoming_all") assertCount2(data.User[16], 2, "incoming", "all", "true")
assertCount(data.User[0], 5, "incoming_confirmed") assertCount2(data.User[16], 1, "incoming", "confirmed", "true")
assertCount(data.User[0], 0, "incoming_unconfirmed") 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)) 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) 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) 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 { func unpointer(v any) any {
if v == nil { if v == nil {
return v return v

View File

@@ -510,7 +510,7 @@ func doUnsubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat
Subscriptions []gin.H `json:"subscriptions"` 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 var subdat gin.H
for _, v := range slist.Subscriptions { 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"` 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 var subdat gin.H
for _, v := range slist.Subscriptions { for _, v := range slist.Subscriptions {

View File

@@ -1,3 +1,8 @@
package util package util
type FormData map[string]string 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() bytesbody = bodybuffer.Bytes()
contentType = writer.FormDataContentType() contentType = writer.FormDataContentType()
case RawJSON:
bytesbody = []byte(body.(RawJSON).Body)
contentType = "application/json"
default: default:
bjson, err := json.Marshal(body) bjson, err := json.Marshal(body)
if err != nil { if err != nil {
@@ -150,7 +153,11 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s
TPrintln("") TPrintln("")
TPrintf("---------------- RESPONSE (%d) ----------------\n", resp.StatusCode) TPrintf("---------------- RESPONSE (%d) ----------------\n", resp.StatusCode)
TPrintln(langext.TryPrettyPrintJson(string(respBodyBin))) if len(respBodyBin) > 100_000 {
TPrintln("[[RESPONSE TOO LONG]]")
} else {
TPrintln(langext.TryPrettyPrintJson(string(respBodyBin)))
}
TryPrintTraceObj("---------------- -------- ----------------", respBodyBin, "") TryPrintTraceObj("---------------- -------- ----------------", respBodyBin, "")
TPrintln("---------------- -------- ----------------") TPrintln("---------------- -------- ----------------")
TPrintln("") TPrintln("")

View File

@@ -75,32 +75,9 @@ func StartSimpleWebserver(t *testing.T) (*logic.Application, string, func()) {
TPrintln("DatabaseFile<requests>: " + dbfile2) TPrintln("DatabaseFile<requests>: " + dbfile2)
TPrintln("DatabaseFile<logs>: " + dbfile3) TPrintln("DatabaseFile<logs>: " + dbfile3)
conf, ok := scn.GetConfig("local-host") scn.Conf = CreateTestConfig(t, dbfile1, dbfile2, dbfile3)
if !ok {
TestFail(t, "conf not found")
}
conf.ServerPort = "0" // simply choose a free port sqlite, err := logic.NewDBPool(scn.Conf)
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)
if err != nil { if err != nil {
TestFailErr(t, err) TestFailErr(t, err)
} }
@@ -111,7 +88,7 @@ func StartSimpleWebserver(t *testing.T) (*logic.Application, string, func()) {
TestFailErr(t, err) TestFailErr(t, err)
} }
ginengine := ginext.NewEngine(conf) ginengine := ginext.NewEngine(scn.Conf)
router := api.NewRouter(app) router := api.NewRouter(app)
@@ -119,7 +96,7 @@ func StartSimpleWebserver(t *testing.T) (*logic.Application, string, func()) {
apc := google.NewDummy() apc := google.NewDummy()
app.Init(conf, ginengine, nc, apc, []logic.Job{ app.Init(scn.Conf, ginengine, nc, apc, []logic.Job{
jobs.NewDeliveryRetryJob(app), jobs.NewDeliveryRetryJob(app),
jobs.NewRequestLogCollectorJob(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 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

@@ -300,7 +300,7 @@ pre, pre span
} }
.display_none { .display_none {
display: none; display: none !important;
} }
#theme-switch { #theme-switch {

View File

@@ -23,12 +23,12 @@
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div> <div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div>
<div class="col-sm-12 col-md"><input placeholder="UserID" id="uid" class="doc" type="number"></div> <div class="col-sm-12 col-md"><input placeholder="UserID" id="uid" class="doc" type="text" pattern="USR[A-Za-z0-9]{21}"></div>
</div> </div>
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="ukey" class="doc">Authentification Key</label></div> <div class="col-sm-12 col-md-3"><label for="ukey" class="doc">Authentification Key</label></div>
<div class="col-sm-12 col-md"><input placeholder="Key" id="ukey" class="doc" type="text" maxlength="64"></div> <div class="col-sm-12 col-md"><input placeholder="Key" id="ukey" class="doc" type="text" pattern="[A-Za-z0-9]{64}"></div>
</div> </div>
<div class="row responsive-label"> <div class="row responsive-label">

View File

@@ -17,7 +17,6 @@ function send()
uid.classList.remove('input-invalid'); uid.classList.remove('input-invalid');
key.classList.remove('input-invalid'); key.classList.remove('input-invalid');
msg.classList.remove('input-invalid');
cnt.classList.remove('input-invalid'); cnt.classList.remove('input-invalid');
pio.classList.remove('input-invalid'); pio.classList.remove('input-invalid');
@@ -30,7 +29,7 @@ function send()
if (cha.value !== '') data.append('channel', cha.value); if (cha.value !== '') data.append('channel', cha.value);
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
xhr.open('POST', '/send.php', true); xhr.open('POST', '/', true);
xhr.onreadystatechange = function () xhr.onreadystatechange = function ()
{ {
if (xhr.readyState !== 4) return; if (xhr.readyState !== 4) return;

View File

@@ -1,8 +1,8 @@
<div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env bash</span> <div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env bash</span>
<span class="c1">#</span> <span class="c1">#</span>
<span class="c1"># Wrapper around SCN ( https://scn.blackforestbytes.com/ )</span> <span class="c1"># Wrapper around SCN ( https://simplecloudnotifier.de/ )</span>
<span class="c1"># ========================================================</span> <span class="c1"># ======================================================</span>
<span class="c1">#</span> <span class="c1">#</span>
<span class="c1"># ./scn_send [@channel] title [content] [priority]</span> <span class="c1"># ./scn_send [@channel] title [content] [priority]</span>
<span class="c1">#</span> <span class="c1">#</span>
@@ -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;</span>
<span class="c1"># or scn_send &quot;@${channel} &quot;${title}&quot; ${content}&quot; &quot;${priority:0|1|2}&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">#</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">################################################################################</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> <span class="c1">################################################################################</span>
usage<span class="o">()</span><span class="w"> </span><span class="o">{</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>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="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">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="nv">$1</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">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">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">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">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="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="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>rederr<span class="w"> </span><span class="s2">&quot;[ERROR]: no title supplied via parameter&quot;</span>
<span class="w"> </span>usage <span class="w"> </span>usage
@@ -51,9 +72,9 @@ usage<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="k">fi</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="w"> </span><span class="o">]]</span><span class="p">;</span><span class="w"> </span><span class="k">then</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="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">channel</span><span class="o">=</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="nv">channel</span><span class="o">=</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="nb">unset</span><span class="w"> </span><span class="s2">&quot;args[0]&quot;</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="w"> </span><span class="nv">channel</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">channel</span><span class="p">:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&quot;</span> <span class="w"> </span><span class="nv">channel</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">channel</span><span class="p">:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">fi</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="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>
@@ -63,24 +84,54 @@ usage<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="k">fi</span> <span class="k">fi</span>
<span class="nv">title</span><span class="o">=</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="nv">title</span><span class="o">=</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="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="nv">content</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="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>-gt<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="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>-gt<span class="w"> </span><span class="m">0</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">content</span><span class="o">=</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="nv">content</span><span class="o">=</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="nb">unset</span><span class="w"> </span><span class="s2">&quot;args[0]&quot;</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">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>-gt<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="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>-gt<span class="w"> </span><span class="m">0</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">priority</span><span class="o">=</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="nv">priority</span><span class="o">=</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="nb">unset</span><span class="w"> </span><span class="s2">&quot;args[0]&quot;</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">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>-gt<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="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>-gt<span class="w"> </span><span class="m">0</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;Too many arguments to scn_send&quot;</span> <span class="w"> </span>rederr<span class="w"> </span><span class="s2">&quot;Too many arguments to scn_send&quot;</span>
<span class="w"> </span>usage <span class="w"> </span>usage
<span class="w"> </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</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">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-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-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-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-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-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> <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>
@@ -89,16 +140,8 @@ usage<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w"> </span><span class="nv">curlresp</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>--silent<span class="w"> </span><span class="se">\</span> <span class="w"> </span><span class="nv">curlresp</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>--silent<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--output<span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">outf</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>--output<span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">outf</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--write-out<span class="w"> </span><span class="s2">&quot;%{http_code}&quot;</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>--write-out<span class="w"> </span><span class="s2">&quot;%{http_code}&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--data<span class="w"> </span><span class="s2">&quot;user_id=</span><span class="nv">$user_id</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">curlparams</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--data<span class="w"> </span><span class="s2">&quot;key=</span><span class="nv">$user_key</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span><span class="s2">&quot;https://simplecloudnotifier.de/&quot;</span><span class="w"> </span><span class="k">)</span>
<span class="w"> </span>--data<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="se">\</span>
<span class="w"> </span>--data<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="se">\</span>
<span class="w"> </span>--data<span class="w"> </span><span class="s2">&quot;content=</span><span class="nv">$content</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--data<span class="w"> </span><span class="s2">&quot;priority=</span><span class="nv">$priority</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--data<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="se">\</span>
<span class="w"> </span>--data<span class="w"> </span><span class="s2">&quot;channel=</span><span class="nv">$channel</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--data<span class="w"> </span><span class="s2">&quot;sender_name=</span><span class="nv">$sender</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span><span class="s2">&quot;https://scn.blackforestbytes.com/&quot;</span><span class="w"> </span><span class="k">)</span>
<span class="w"> </span><span class="nv">curlout</span><span class="o">=</span><span class="s2">&quot;</span><span class="k">$(</span>cat<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$outf</span><span class="s2">&quot;</span><span class="k">)</span><span class="s2">&quot;</span> <span class="w"> </span><span class="nv">curlout</span><span class="o">=</span><span class="s2">&quot;</span><span class="k">$(</span>cat<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$outf</span><span class="s2">&quot;</span><span class="k">)</span><span class="s2">&quot;</span>
<span class="w"> </span>rm<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$outf</span><span class="s2">&quot;</span> <span class="w"> </span>rm<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$outf</span><span class="s2">&quot;</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',
],
];

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