Compare commits
42 Commits
backend-re
...
android_ap
Author | SHA1 | Date | |
---|---|---|---|
a12584d636
|
|||
86ad6986b1
|
|||
c0b5119a8e
|
|||
9f3e183d72
|
|||
51f5f1005a
|
|||
0a380f861e
|
|||
b712ad3488
|
|||
9f656bdefe
|
|||
a4a651229c
|
|||
4773800f23
|
|||
bef0b8189e
|
|||
674714f0f3
|
|||
ee9e858584
|
|||
165c6d8614
|
|||
8a6719fc19
|
|||
308361a834
|
|||
44df964f6f
|
|||
56bf266919
|
|||
f3658d6636
|
|||
1bb37eec30
|
|||
59511b2345
|
|||
5b7bc02c61
|
|||
b329f537e7
|
|||
5879e81759
|
|||
f4e88bef77
|
|||
b3ec45309c
|
|||
2fbc892898
|
|||
c46190c3fc
|
|||
860e540de1
|
|||
8cde286cac
|
|||
90830fe384
|
|||
686f89f75d
|
|||
4210af5680
|
|||
aefc368cfd
|
|||
67218d8045
|
|||
c05deb3a41
|
|||
43d0107fb5
|
|||
ece7612f9d
|
|||
a9809d90cb
|
|||
bbc9a79996
|
|||
b71f1885ec
|
|||
885aad2047
|
43
.gitea/workflows/build_and_deploy.yml
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
15
android_v2/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
3
android_v2/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
1
android_v2/.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Simplecloudnotifier2
|
119
android_v2/.idea/codeStyles
generated
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
android_v2/.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="17" />
|
||||||
|
</component>
|
||||||
|
</project>
|
19
android_v2/.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="GRADLE" />
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
46
android_v2/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||||
|
<option name="processCode" value="true" />
|
||||||
|
<option name="processLiterals" value="true" />
|
||||||
|
<option name="processComments" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
android_v2/.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.8.10" />
|
||||||
|
</component>
|
||||||
|
</project>
|
10
android_v2/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
6
android_v2/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
1
android_v2/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
66
android_v2/app/build.gradle.kts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.blackforestbytes.simplecloudnotifier2"
|
||||||
|
compileSdk = 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.blackforestbytes.simplecloudnotifier2"
|
||||||
|
minSdk = 29
|
||||||
|
targetSdk = 33
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
vectorDrawables {
|
||||||
|
useSupportLibrary = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.4.3"
|
||||||
|
}
|
||||||
|
packaging {
|
||||||
|
resources {
|
||||||
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
|
||||||
|
implementation("androidx.activity:activity-compose:1.7.0")
|
||||||
|
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
|
||||||
|
implementation("androidx.compose.ui:ui")
|
||||||
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
|
implementation("androidx.compose.material3:material3")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
|
||||||
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
|
}
|
21
android_v2/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
@@ -0,0 +1,24 @@
|
|||||||
|
package com.blackforestbytes.simplecloudnotifier2
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("com.blackforestbytes.simplecloudnotifier2", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
28
android_v2/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.Simplecloudnotifier2"
|
||||||
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.Simplecloudnotifier2">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
@@ -0,0 +1,221 @@
|
|||||||
|
package com.blackforestbytes.simplecloudnotifier2
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FabPosition
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.blackforestbytes.simplecloudnotifier2.ui.theme.Simplecloudnotifier2Theme
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
Simplecloudnotifier2Theme {
|
||||||
|
// A surface container using the 'background' color from the theme
|
||||||
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
|
|
||||||
|
Content()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Content() {
|
||||||
|
Scaffold(
|
||||||
|
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
title = {
|
||||||
|
Text("Messages", maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { /* do something */ }) {
|
||||||
|
Icon(painterResource(R.drawable.fas_gauge), contentDescription = "Menu", modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { /* do something */ }) {
|
||||||
|
Icon(painterResource(R.drawable.fas_paper_plane_top), contentDescription = "Send message", modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
bottomBar = { NavBar() },
|
||||||
|
|
||||||
|
floatingActionButton = { NavFAB() },
|
||||||
|
|
||||||
|
floatingActionButtonPosition = FabPosition.Center,
|
||||||
|
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
MessageCard()
|
||||||
|
MessageCard()
|
||||||
|
MessageCard()
|
||||||
|
MessageCard()
|
||||||
|
MessageCard()
|
||||||
|
MessageCard()
|
||||||
|
MessageCard()
|
||||||
|
MessageCard()
|
||||||
|
MessageCard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavBar() {
|
||||||
|
NavigationBar {
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { Icon(painterResource(R.drawable.fas_road), contentDescription = "Channels", modifier = Modifier.size(32.dp)) },
|
||||||
|
onClick = {},
|
||||||
|
selected = false,
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { Icon(painterResource(R.drawable.fas_computer), contentDescription = "Clients", modifier = Modifier.size(32.dp)) },
|
||||||
|
onClick = {},
|
||||||
|
selected = false,
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { Icon(painterResource(R.drawable.fas_key), contentDescription = "Keys", modifier = Modifier.size(32.dp)) },
|
||||||
|
onClick = {},
|
||||||
|
selected = false,
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { },
|
||||||
|
onClick = {},
|
||||||
|
selected = false,
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { Icon(painterResource(R.drawable.fas_bookmark), contentDescription = "Subscriptions", modifier = Modifier.size(32.dp)) },
|
||||||
|
onClick = {},
|
||||||
|
selected = false,
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { Icon(painterResource(R.drawable.fas_user), contentDescription = "User", modifier = Modifier.size(32.dp)) },
|
||||||
|
onClick = {},
|
||||||
|
selected = false,
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = { Icon(painterResource(R.drawable.fas_gear), contentDescription = "Settings", modifier = Modifier.size(32.dp)) },
|
||||||
|
onClick = {},
|
||||||
|
selected = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavFAB() {
|
||||||
|
Box(){
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { /* stub */ },
|
||||||
|
shape = FloatingActionButtonDefaults.shape,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(70.dp)
|
||||||
|
.offset(y = 50.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.fas_plus),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(45.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessageCard() {
|
||||||
|
ElevatedCard(
|
||||||
|
elevation = CardDefaults.cardElevation(
|
||||||
|
defaultElevation = 6.dp
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth().height(height = 100.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Channel",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Title",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Body",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Date",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun GreetingPreview() {
|
||||||
|
Simplecloudnotifier2Theme {
|
||||||
|
|
||||||
|
Content()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
package com.blackforestbytes.simplecloudnotifier2.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val Purple80 = Color(0xFFD0BCFF)
|
||||||
|
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||||
|
val Pink80 = Color(0xFFEFB8C8)
|
||||||
|
|
||||||
|
val Purple40 = Color(0xFF6650a4)
|
||||||
|
val PurpleGrey40 = Color(0xFF625b71)
|
||||||
|
val Pink40 = Color(0xFF7D5260)
|
@@ -0,0 +1,70 @@
|
|||||||
|
package com.blackforestbytes.simplecloudnotifier2.ui.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = Purple80,
|
||||||
|
secondary = PurpleGrey80,
|
||||||
|
tertiary = Pink80
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LightColorScheme = lightColorScheme(
|
||||||
|
primary = Purple40,
|
||||||
|
secondary = PurpleGrey40,
|
||||||
|
tertiary = Pink40
|
||||||
|
|
||||||
|
/* Other default colors to override
|
||||||
|
background = Color(0xFFFFFBFE),
|
||||||
|
surface = Color(0xFFFFFBFE),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onSecondary = Color.White,
|
||||||
|
onTertiary = Color.White,
|
||||||
|
onBackground = Color(0xFF1C1B1F),
|
||||||
|
onSurface = Color(0xFF1C1B1F),
|
||||||
|
*/
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Simplecloudnotifier2Theme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
// Dynamic color is available on Android 12+
|
||||||
|
dynamicColor: Boolean = true,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
darkTheme -> DarkColorScheme
|
||||||
|
else -> LightColorScheme
|
||||||
|
}
|
||||||
|
val view = LocalView.current
|
||||||
|
if (!view.isInEditMode) {
|
||||||
|
SideEffect {
|
||||||
|
val window = (view.context as Activity).window
|
||||||
|
window.statusBarColor = colorScheme.primary.toArgb()
|
||||||
|
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
package com.blackforestbytes.simplecloudnotifier2.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
// Set of Material typography styles to start with
|
||||||
|
val Typography = Typography(
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
/* Other default text styles to override
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
9
android_v2/app/src/main/res/drawable/fas_bookmark.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="384dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="384"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M0,48V487.7C0,501.1 10.9,512 24.3,512c5,0 9.9,-1.5 14,-4.4L192,400 345.7,507.6c4.1,2.9 9,4.4 14,4.4c13.4,0 24.3,-10.9 24.3,-24.3V48c0,-26.5 -21.5,-48 -48,-48H48C21.5,0 0,21.5 0,48z"/>
|
||||||
|
</vector>
|
9
android_v2/app/src/main/res/drawable/fas_burger.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="512dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M61.1,224C45,224 32,211 32,194.9c0,-1.9 0.2,-3.7 0.6,-5.6C37.9,168.3 78.8,32 256,32s218.1,136.3 223.4,157.3c0.5,1.9 0.6,3.7 0.6,5.6c0,16.1 -13,29.1 -29.1,29.1L61.1,224zM144,128a16,16 0,1 0,-32 0,16 16,0 1,0 32,0zM384,144a16,16 0,1 0,0 -32,16 16,0 1,0 0,32zM272,96a16,16 0,1 0,-32 0,16 16,0 1,0 32,0zM16,304c0,-26.5 21.5,-48 48,-48L448,256c26.5,0 48,21.5 48,48s-21.5,48 -48,48L64,352c-26.5,0 -48,-21.5 -48,-48zM32,400c0,-8.8 7.2,-16 16,-16L464,384c8.8,0 16,7.2 16,16v16c0,35.3 -28.7,64 -64,64L96,480c-35.3,0 -64,-28.7 -64,-64L32,400z"/>
|
||||||
|
</vector>
|
9
android_v2/app/src/main/res/drawable/fas_computer.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="640dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="640"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M384,96L384,320L64,320L64,96L384,96zM64,32C28.7,32 0,60.7 0,96L0,320c0,35.3 28.7,64 64,64L181.3,384l-10.7,32L96,416c-17.7,0 -32,14.3 -32,32s14.3,32 32,32L352,480c17.7,0 32,-14.3 32,-32s-14.3,-32 -32,-32L277.3,416l-10.7,-32L384,384c35.3,0 64,-28.7 64,-64L448,96c0,-35.3 -28.7,-64 -64,-64L64,32zM528,32c-26.5,0 -48,21.5 -48,48L480,432c0,26.5 21.5,48 48,48h64c26.5,0 48,-21.5 48,-48L640,80c0,-26.5 -21.5,-48 -48,-48L528,32zM544,96h32c8.8,0 16,7.2 16,16s-7.2,16 -16,16L544,128c-8.8,0 -16,-7.2 -16,-16s7.2,-16 16,-16zM528,176c0,-8.8 7.2,-16 16,-16h32c8.8,0 16,7.2 16,16s-7.2,16 -16,16L544,192c-8.8,0 -16,-7.2 -16,-16zM560,336a32,32 0,1 1,0 64,32 32,0 1,1 0,-64z"/>
|
||||||
|
</vector>
|
9
android_v2/app/src/main/res/drawable/fas_gauge.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="512dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M0,256a256,256 0,1 1,512 0A256,256 0,1 1,0 256zM320,352c0,-26.9 -16.5,-49.9 -40,-59.3L280,88c0,-13.3 -10.7,-24 -24,-24s-24,10.7 -24,24L232,292.7c-23.5,9.5 -40,32.5 -40,59.3c0,35.3 28.7,64 64,64s64,-28.7 64,-64zM144,176a32,32 0,1 0,0 -64,32 32,0 1,0 0,64zM128,256a32,32 0,1 0,-64 0,32 32,0 1,0 64,0zM416,288a32,32 0,1 0,0 -64,32 32,0 1,0 0,64zM400,144a32,32 0,1 0,-64 0,32 32,0 1,0 64,0z"/>
|
||||||
|
</vector>
|
9
android_v2/app/src/main/res/drawable/fas_gear.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="512dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M495.9,166.6c3.2,8.7 0.5,18.4 -6.4,24.6l-43.3,39.4c1.1,8.3 1.7,16.8 1.7,25.4s-0.6,17.1 -1.7,25.4l43.3,39.4c6.9,6.2 9.6,15.9 6.4,24.6c-4.4,11.9 -9.7,23.3 -15.8,34.3l-4.7,8.1c-6.6,11 -14,21.4 -22.1,31.2c-5.9,7.2 -15.7,9.6 -24.5,6.8l-55.7,-17.7c-13.4,10.3 -28.2,18.9 -44,25.4l-12.5,57.1c-2,9.1 -9,16.3 -18.2,17.8c-13.8,2.3 -28,3.5 -42.5,3.5s-28.7,-1.2 -42.5,-3.5c-9.2,-1.5 -16.2,-8.7 -18.2,-17.8l-12.5,-57.1c-15.8,-6.5 -30.6,-15.1 -44,-25.4L83.1,425.9c-8.8,2.8 -18.6,0.3 -24.5,-6.8c-8.1,-9.8 -15.5,-20.2 -22.1,-31.2l-4.7,-8.1c-6.1,-11 -11.4,-22.4 -15.8,-34.3c-3.2,-8.7 -0.5,-18.4 6.4,-24.6l43.3,-39.4C64.6,273.1 64,264.6 64,256s0.6,-17.1 1.7,-25.4L22.4,191.2c-6.9,-6.2 -9.6,-15.9 -6.4,-24.6c4.4,-11.9 9.7,-23.3 15.8,-34.3l4.7,-8.1c6.6,-11 14,-21.4 22.1,-31.2c5.9,-7.2 15.7,-9.6 24.5,-6.8l55.7,17.7c13.4,-10.3 28.2,-18.9 44,-25.4l12.5,-57.1c2,-9.1 9,-16.3 18.2,-17.8C227.3,1.2 241.5,0 256,0s28.7,1.2 42.5,3.5c9.2,1.5 16.2,8.7 18.2,17.8l12.5,57.1c15.8,6.5 30.6,15.1 44,25.4l55.7,-17.7c8.8,-2.8 18.6,-0.3 24.5,6.8c8.1,9.8 15.5,20.2 22.1,31.2l4.7,8.1c6.1,11 11.4,22.4 15.8,34.3zM256,336a80,80 0,1 0,0 -160,80 80,0 1,0 0,160z"/>
|
||||||
|
</vector>
|
9
android_v2/app/src/main/res/drawable/fas_key.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="512dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M336,352c97.2,0 176,-78.8 176,-176S433.2,0 336,0S160,78.8 160,176c0,18.7 2.9,36.8 8.3,53.7L7,391c-4.5,4.5 -7,10.6 -7,17v80c0,13.3 10.7,24 24,24h80c13.3,0 24,-10.7 24,-24V448h40c13.3,0 24,-10.7 24,-24V384h40c6.4,0 12.5,-2.5 17,-7l33.3,-33.3c16.9,5.4 35,8.3 53.7,8.3zM376,96a40,40 0,1 1,0 80,40 40,0 1,1 0,-80z"/>
|
||||||
|
</vector>
|
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="512dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M49.9,27.8C15.1,12.7 -19.2,50.1 -1.2,83.5L68.1,212.2c4.4,8.3 12.6,13.8 21.9,15c0,0 0,0 0,0l176,22c3.4,0.4 6,3.3 6,6.7s-2.6,6.3 -6,6.7l-176,22s0,0 0,0c-9.3,1.2 -17.5,6.8 -21.9,15L-1.2,428.5c-18,33.4 16.3,70.8 51.1,55.7L491.8,292.7c32.1,-13.9 32.1,-59.5 0,-73.4L49.9,27.8z"/>
|
||||||
|
</vector>
|
9
android_v2/app/src/main/res/drawable/fas_plus.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="448dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="448"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M256,80c0,-17.7 -14.3,-32 -32,-32s-32,14.3 -32,32V224H48c-17.7,0 -32,14.3 -32,32s14.3,32 32,32H192V432c0,17.7 14.3,32 32,32s32,-14.3 32,-32V288H400c17.7,0 32,-14.3 32,-32s-14.3,-32 -32,-32H256V80z"/>
|
||||||
|
</vector>
|
9
android_v2/app/src/main/res/drawable/fas_road.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="576dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="576"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M256,32L181.2,32c-27.1,0 -51.3,17.1 -60.3,42.6L3.1,407.2C1.1,413 0,419.2 0,425.4C0,455.5 24.5,480 54.6,480L256,480L256,416c0,-17.7 14.3,-32 32,-32s32,14.3 32,32v64L521.4,480c30.2,0 54.6,-24.5 54.6,-54.6c0,-6.2 -1.1,-12.4 -3.1,-18.2L455.1,74.6C446,49.1 421.9,32 394.8,32L320,32L320,96c0,17.7 -14.3,32 -32,32s-32,-14.3 -32,-32L256,32zM320,224v64c0,17.7 -14.3,32 -32,32s-32,-14.3 -32,-32L256,224c0,-17.7 14.3,-32 32,-32s32,14.3 32,32z"/>
|
||||||
|
</vector>
|
9
android_v2/app/src/main/res/drawable/fas_sack.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="512dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M192,96L320,96l47.4,-71.1C374.5,14.2 366.9,0 354.1,0L157.9,0c-12.8,0 -20.4,14.2 -13.3,24.9L192,96zM320,128L192,128c-3.8,2.5 -8.1,5.3 -13,8.4l0,0C122.3,172.7 0,250.9 0,416c0,53 43,96 96,96L416,512c53,0 96,-43 96,-96c0,-165.1 -122.3,-243.3 -179,-279.6c-4.8,-3.1 -9.2,-5.9 -13,-8.4z"/>
|
||||||
|
</vector>
|
9
android_v2/app/src/main/res/drawable/fas_user.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="448dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="448"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M224,256A128,128 0,1 0,224 0a128,128 0,1 0,0 256zM178.3,304C79.8,304 0,383.8 0,482.3C0,498.7 13.3,512 29.7,512L418.3,512c16.4,0 29.7,-13.3 29.7,-29.7C448,383.8 368.2,304 269.7,304L178.3,304z"/>
|
||||||
|
</vector>
|
170
android_v2/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
@@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
BIN
android_v2/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
android_v2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
android_v2/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 982 B |
BIN
android_v2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
android_v2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
android_v2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
android_v2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
android_v2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
android_v2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 7.6 KiB |
10
android_v2/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
3
android_v2/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Simplecloudnotifier2</string>
|
||||||
|
</resources>
|
5
android_v2/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.Simplecloudnotifier2" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
|
</resources>
|
13
android_v2/app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample backup rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/guide/topics/data/autobackup
|
||||||
|
for details.
|
||||||
|
Note: This file is ignored for devices older that API 31
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore
|
||||||
|
-->
|
||||||
|
<full-backup-content>
|
||||||
|
<!--
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
|
-->
|
||||||
|
</full-backup-content>
|
19
android_v2/app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
for details.
|
||||||
|
-->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
-->
|
||||||
|
</cloud-backup>
|
||||||
|
<!--
|
||||||
|
<device-transfer>
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
</device-transfer>
|
||||||
|
-->
|
||||||
|
</data-extraction-rules>
|
@@ -0,0 +1,17 @@
|
|||||||
|
package com.blackforestbytes.simplecloudnotifier2
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
5
android_v2/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
id("com.android.application") version "8.1.3" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
|
||||||
|
}
|
23
android_v2/gradle.properties
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
BIN
android_v2/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
android_v2/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#Sat Nov 18 19:33:07 CET 2023
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
185
android_v2/gradlew
vendored
Executable file
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
89
android_v2/gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
18
android_v2/settings.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "Simplecloudnotifier2"
|
||||||
|
include(":app")
|
||||||
|
|
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GoLinterSettings">
|
||||||
|
<option name="checkGoLinterExe" value="false" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@@ -4,11 +4,13 @@ FROM golang:1-bullseye AS builder
|
|||||||
|
|
||||||
RUN apt-get update && \
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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...
|
||||||
|
20
scnserver/_gen/id-generate.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
|
||||||
}
|
|
||||||
|
134
scnserver/api/handler/external.go
Normal 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,
|
||||||
|
}))
|
||||||
|
}
|
@@ -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
|
|
||||||
}
|
|
||||||
|
@@ -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))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================
|
// ================
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
233
scnserver/db/schema/primary_4.ddl
Normal 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)
|
20
scnserver/db/schema/primary_migration_3_4.ddl
Normal 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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -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) {
|