19 Commits

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

View File

@@ -0,0 +1,43 @@
# https://docs.gitea.com/next/usage/actions/quickstart
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
name: Build Docker and Deploy
run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }}
on:
push:
branches: ['master']
jobs:
build_job:
name: Build Docker Container
runs-on: bfb-cicd-latest
steps:
- run: echo -n "${{ secrets.DOCKER_REG_PASS }}" | docker login registry.blackforestbytes.com -u docker --password-stdin
- name: Check out code
uses: actions/checkout@v3
- run: cd "${{ gitea.workspace }}/scnserver" && make clean
- run: cd "${{ gitea.workspace }}/scnserver" && make docker
- run: cd "${{ gitea.workspace }}/scnserver" && make push-docker
deploy_job:
name: Deploy to Server
needs: [build_job]
runs-on: ubuntu-latest
steps:
- name: Execute deploy on remote (via ssh)
uses: appleboy/ssh-action@v1.0.0
with:
host: simplecloudnotifier.de
username: bfb-deploy-bot
port: 4477
key: "${{ secrets.SSH_KEY_BFBDEPLOYBOT }}"
script: cd /var/docker/deploy-scripts/simplecloudnotifier && ./deploy.sh master "${{ gitea.sha }}" || exit 1

15
android_v2/.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
android_v2/.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
Simplecloudnotifier2

119
android_v2/.idea/codeStyles generated Normal file
View 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
View 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
View 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>

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
/build

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

View 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="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>

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

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

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

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

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

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

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Simplecloudnotifier2</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Simplecloudnotifier2" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

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

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

View File

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

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

View 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

Binary file not shown.

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

View 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")

View File

@@ -14,13 +14,10 @@
# or scn_send "@${channel} "${title}" ${content}"
# or scn_send "@${channel} "${title}" ${content}" "${priority:0|1|2}"
#
# content can be of format "--scnsend-read-body-from-file={path}" to read body from file
# (this circumvents max commandline length)
#
################################################################################
# INSERT YOUR DATA HERE #
################################################################################
user_id="999" # your user_id
user_key="??" # use userkey with SEND permissions on the used channel
################################################################################
usage() {
@@ -34,16 +31,40 @@ function cfgcol { [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge
function rederr() { if cfgcol; then >&2 echo -e "\x1B[31m$1\x1B[0m"; else >&2 echo "$1"; fi; }
function green() { if cfgcol; then echo -e "\x1B[32m$1\x1B[0m"; else echo "$1"; fi; }
################################################################################
#
# Get env 'SCN_UID' and 'SCN_KEY' from conf file
#
# shellcheck source=/dev/null
. "/etc/scn.conf"
SCN_UID=${SCN_UID:-}
SCN_KEY=${SCN_KEY:-}
[ -z "${SCN_UID}" ] && { rederr "Missing config value 'SCN_UID' in /etc/scn.conf"; exit 1; }
[ -z "${SCN_KEY}" ] && { rederr "Missing config value 'SCN_KEY' in /etc/scn.conf"; exit 1; }
################################################################################
args=( "$@" )
title=""
content=""
channel=""
priority=1
priority=""
usr_msg_id="$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)"
sendtime="$(date +%s)"
sender="$(hostname)"
if command -v srvname &> /dev/null; then
sender="$( srvname )"
fi
if [[ "${args[0]}" = "--" ]]; then
# only positional args form here on (currently not handled)
args=("${args[@]:1}")
fi
if [ ${#args[@]} -lt 1 ]; then
rederr "[ERROR]: no title supplied via parameter"
usage
@@ -83,28 +104,33 @@ if [ ${#args[@]} -gt 0 ]; then
exit 1
fi
if [[ "$content" == --scnsend-read-body-from-file=* ]]; then
path="$( awk '{ print substr($0, 31) }' <<< "$content" )"
content="$( cat "$path" )"
fi
curlparams=()
curlparams+=( "--data" "user_id=${SCN_UID}" )
curlparams+=( "--data" "key=${SCN_KEY}" )
curlparams+=( "--data" "title=$title" )
curlparams+=( "--data" "timestamp=$sendtime" )
curlparams+=( "--data" "msg_id=$usr_msg_id" )
curlparams+=( "--data-urlencode" "user_id=${SCN_UID}" )
curlparams+=( "--data-urlencode" "key=${SCN_KEY}" )
curlparams+=( "--data-urlencode" "title=$title" )
curlparams+=( "--data-urlencode" "timestamp=$sendtime" )
curlparams+=( "--data-urlencode" "msg_id=$usr_msg_id" )
if [[ -n "$content" ]]; then
curlparams+=("--data" "content=$content")
curlparams+=("--data-urlencode" "content=$content")
fi
if [[ -n "$priority" ]]; then
curlparams+=("--data" "priority=$priority")
curlparams+=("--data-urlencode" "priority=$priority")
fi
if [[ -n "$channel" ]]; then
curlparams+=("--data" "channel=$channel")
curlparams+=("--data-urlencode" "channel=$channel")
fi
if [[ -n "$sender" ]]; then
curlparams+=("--data" "sender_name=$sender")
curlparams+=("--data-urlencode" "sender_name=$sender")
fi
while true ; do

View File

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

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

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

View File

@@ -4,11 +4,13 @@ FROM golang:1-bullseye AS builder
RUN apt-get update && \
apt-get install -y ca-certificates openssl make git tar coreutils && \
apt-get install -y python3 python3-pip && \
pip install virtualenv && \
rm -rf /var/lib/apt/lists/*
COPY . /buildsrc
RUN cd /buildsrc && make build
RUN cd /buildsrc && cp "scn_send.sh" "../scn_send.sh" && make build

View File

@@ -7,12 +7,20 @@ HASH=$(shell git rev-parse HEAD)
.PHONY: test swagger pygmentize docker migrate dgi pygmentize lint docker
build: swagger pygmentize fmt
SWAGGO_VERSION=v1.8.12
SWAGGO=github.com/swaggo/swag/cmd/swag@$(SWAGGO_VERSION)
build: ids enums swagger pygmentize fmt
mkdir -p _build
rm -f ./_build/scn_backend
go generate ./...
CGO_ENABLED=1 go build -v -o _build/scn_backend -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" ./cmd/scnserver
enums:
go generate models/enums.go
ids:
go generate models/ids.go
run: build
mkdir -p .run-data
_build/scn_backend
@@ -30,6 +38,7 @@ dgi:
echo -n "REMOTE=" >> DOCKER_GIT_INFO ; git config --get remote.origin.url >> DOCKER_GIT_INFO
docker: dgi
cp ../scn_send.sh .
docker build \
-t "$(DOCKER_NAME):$(HASH)" \
-t "$(DOCKER_NAME):$(NAMESPACE)-latest" \
@@ -38,17 +47,21 @@ docker: dgi
-t "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest" \
-t "$(DOCKER_REPO)/$(DOCKER_NAME):latest" \
.
[ -f "scn_send.sh" ] && rm scn_send.sh
swagger:
which swag || go install github.com/swaggo/swag/cmd/swag@v1.8.12
swag init -generalInfo api/router.go --propertyStrategy snakecase --output ./swagger/ --outputTypes "json,yaml"
swagger-setup:
mkdir -p ".swaggobin"
[ -f ".swaggobin/swag_$(SWAGGO_VERSION)" ] || { GOBIN=/tmp/_swaggo go install $(SWAGGO); cp "/tmp/_swaggo/swag" ".swaggobin/swag_$(SWAGGO_VERSION)"; rm -rf "/tmp/_swaggo"; }
swagger: swagger-setup
".swaggobin/swag_$(SWAGGO_VERSION)" init -generalInfo ./api/router.go --propertyStrategy camelcase --output ./swagger/ --outputTypes "json,yaml"
pygmentize: website/scn_send.html
website/scn_send.html: website/scn_send.sh.txt
_pygments/pygmentizew -l bash -f html "$(shell pwd)/website/scn_send.sh.txt" > "$(shell pwd)/website/scn_send.html"
_pygments/pygmentizew -S monokai -f html > "$(shell pwd)/website/css/pygmnetize-dark.css"
_pygments/pygmentizew -S borland -f html > "$(shell pwd)/website/css/pygmnetize-light.css"
website/scn_send.html: ../scn_send.sh
_pygments/pygmentizew -l bash -f html "$(shell pwd)/../scn_send.sh" > "$(shell pwd)/website/scn_send.html"
_pygments/pygmentizew -S monokai -f html > "$(shell pwd)/website/css/pygmnetize-dark.css"
_pygments/pygmentizew -S borland -f html > "$(shell pwd)/website/css/pygmnetize-light.css"
run-docker-local: docker
mkdir -p .run-data
@@ -67,7 +80,7 @@ inspect-docker: docker
$(DOCKER_NAME):latest \
bash
push-docker: docker
push-docker:
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(HASH)"
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest"
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):latest"
@@ -75,13 +88,14 @@ push-docker: docker
clean:
rm -rf _build/*
rm -rf .run-data/*
rm -rf _pygments/env
git clean -fdx
go clean
go clean -testcache
! which go 2>&1 >> /dev/null || go clean
! which go 2>&1 >> /dev/null || go clean -testcache
fmt:
fmt: swagger-setup
go fmt ./...
swag fmt
".swaggobin/swag_$(SWAGGO_VERSION)" fmt
test:
which gotestsum || go install gotest.tools/gotestsum@latest

View File

@@ -4,14 +4,18 @@
========
#### BEFORE RELEASE
#### DO DO DO
- app-store link in HTML
- backups (no longer container in my db_backup, perhaps extend it to sqlite?)
- ios purchase verification
- (!) use goext.ginWrapper
- (!) use goext.exerr
- use bfcodegen (enums+id)
#### UNSURE
- (?) default-priority for channels
@@ -47,10 +51,6 @@
- route to re-check all pro-token (for me)
- /send endpoint should be compatible with the [ webhook ] notifier of uptime-kuma
(or add another /kuma endpoint)
-> https://webhook.site/
- endpoint to list all servernames of user (distinct select)
- weblogin, webapp, ...
@@ -64,6 +64,10 @@
- use job superclass (copy from isi/bnet/?), reduce duplicate code
- admin panel (especially errors and requests)
- cli app (?)
#### FUTURE
- Remove compat, especially do not create compat id for every new message...

View File

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

View File

@@ -6,6 +6,7 @@ import (
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
@@ -146,7 +147,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
}
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if err != nil {
@@ -206,7 +207,7 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
}
user, err := h.database.GetUser(ctx, u.UserID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
}
if err != nil {
@@ -298,7 +299,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
}
_, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if err != nil {
@@ -306,7 +307,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
}
user, err := h.database.GetUser(ctx, u.UserID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
}
if err != nil {
@@ -348,8 +349,8 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
descName = langext.Ptr(strings.TrimSpace(*b.DescriptionName))
}
if descName != nil && len(*descName) > user.MaxChannelDescriptionNameLength() {
return ginresp.APIError(g, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG, fmt.Sprintf("Channel-Description too long (max %d characters)", user.MaxChannelNameLength()), nil)
if descName != nil && len(*descName) > user.MaxChannelDescriptionLength() {
return ginresp.APIError(g, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG, fmt.Sprintf("Channel-Description too long (max %d characters)", user.MaxChannelDescriptionLength()), nil)
}
err := h.database.UpdateChannelDescriptionName(ctx, u.ChannelID, descName)
@@ -420,7 +421,7 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize)
channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID, false)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http"
@@ -87,7 +88,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
}
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
}
if err != nil {
@@ -192,7 +193,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
}
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
}
if err != nil {
@@ -251,7 +252,7 @@ func (h APIHandler) UpdateClient(g *gin.Context) ginresp.HTTPResponse {
}
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
}
if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http"
@@ -90,7 +91,7 @@ func (h APIHandler) GetUserKey(g *gin.Context) ginresp.HTTPResponse {
}
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
}
if err != nil {
@@ -143,7 +144,7 @@ func (h APIHandler) UpdateUserKey(g *gin.Context) ginresp.HTTPResponse {
}
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
}
if err != nil {
@@ -302,7 +303,7 @@ func (h APIHandler) DeleteUserKey(g *gin.Context) ginresp.HTTPResponse {
}
client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
}
if err != nil {

View File

@@ -1,17 +1,19 @@
package handler
import (
"database/sql"
"errors"
"net/http"
"strings"
"time"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
"net/http"
"strings"
"time"
)
// ListMessages swaggerdoc
@@ -173,7 +175,7 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/messages/{mid} [PATCH]
// @Router /api/v2/messages/{mid} [GET]
func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
MessageID models.MessageID `uri:"mid" binding:"entityid"`
@@ -191,7 +193,7 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
}
msg, err := h.database.GetMessage(ctx, u.MessageID, false)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
}
if err != nil {
@@ -259,14 +261,14 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
}
msg, err := h.database.GetMessage(ctx, u.MessageID, false)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err)
}
if !ctx.CheckPermissionMessageRead(msg) {
if !ctx.CheckPermissionMessageDelete(msg) {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}

View File

@@ -5,6 +5,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http"
@@ -14,13 +15,25 @@ import (
// ListUserSubscriptions swaggerdoc
//
// @Summary List all subscriptions of a user (incoming/owned)
// @Description The possible values for 'selector' are:
// @Description - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)
// @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber
// @Description - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber
// @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
// @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
// @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
//
// @Description The possible values for 'direction' are:
// @Description - "outgoing" Subscriptions with the user as subscriber (= subscriptions he can use to read channels)
// @Description - "incoming" Subscriptions to channels of this user (= incoming subscriptions and subscription requests)
// @Description - "both" Combines "outgoing" and "incoming" (default)
// @Description
// @Description The possible values for 'confirmation' are:
// @Description - "confirmed" Confirmed (active) subscriptions
// @Description - "unconfirmed" Unconfirmed (pending) subscriptions
// @Description - "all" Combines "confirmed" and "unconfirmed" (default)
// @Description
// @Description The possible values for 'external' are:
// @Description - "true" Subscriptions with subscriber_user_id != channel_owner_user_id (subscriptions from other users)
// @Description - "false" Subscriptions with subscriber_user_id == channel_owner_user_id (subscriptions from this user to his own channels)
// @Description - "all" Combines "external" and "internal" (default)
// @Description
// @Description The `subscriber_user_id` parameter can be used to additionally filter the subscriber_user_id (return subscribtions from a specific user)
// @Description
// @Description The `channel_owner_user_id` parameter can be used to additionally filter the channel_owner_user_id (return subscribtions to a specific user)
//
// @ID api-user-subscriptions-list
// @Tags API-v2
@@ -39,7 +52,11 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
UserID models.UserID `uri:"uid" binding:"entityid"`
}
type query struct {
Selector *string `json:"selector" form:"selector" enums:"outgoing_all,outgoing_confirmed,outgoing_unconfirmed,incoming_all,incoming_confirmed,incoming_unconfirmed"`
Direction *string `json:"direction" form:"direction" enums:"incoming,outgoing,both"`
Confirmation *string `json:"confirmation" form:"confirmation" enums:"confirmed,unconfirmed,all"`
External *string `json:"external" form:"external" enums:"true,false,all"`
SubscriberUserID *models.UserID `json:"subscriber_user_id" form:"subscriber_user_id"`
ChannelOwnerUserID *models.UserID `json:"channel_owner_user_id" form:"channel_owner_user_id"`
}
type response struct {
Subscriptions []models.SubscriptionJSON `json:"subscriptions"`
@@ -57,57 +74,56 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
return *permResp
}
sel := strings.ToLower(langext.Coalesce(q.Selector, "outgoing_all"))
filter := models.SubscriptionFilter{}
filter.AnyUserID = langext.Ptr(u.UserID)
var res []models.Subscription
var err error
if sel == "outgoing_all" {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
if q.Direction != nil {
if strings.EqualFold(*q.Direction, "incoming") {
filter.ChannelOwnerUserID = langext.Ptr([]models.UserID{u.UserID})
} else if strings.EqualFold(*q.Direction, "outgoing") {
filter.SubscriberUserID = langext.Ptr([]models.UserID{u.UserID})
} else if strings.EqualFold(*q.Direction, "both") {
// both
} else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'direction'", nil)
}
}
} else if sel == "outgoing_confirmed" {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
if q.Confirmation != nil {
if strings.EqualFold(*q.Confirmation, "confirmed") {
filter.Confirmed = langext.PTrue
} else if strings.EqualFold(*q.Confirmation, "unconfirmed") {
filter.Confirmed = langext.PFalse
} else if strings.EqualFold(*q.Confirmation, "all") {
// both
} else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'confirmation'", nil)
}
}
} else if sel == "outgoing_unconfirmed" {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
if q.External != nil {
if strings.EqualFold(*q.External, "true") {
filter.SubscriberIsChannelOwner = langext.PFalse
} else if strings.EqualFold(*q.External, "false") {
filter.SubscriberIsChannelOwner = langext.PTrue
} else if strings.EqualFold(*q.External, "all") {
// both
} else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'external'", nil)
}
}
} else if sel == "incoming_all" {
if q.SubscriberUserID != nil {
filter.SubscriberUserID2 = langext.Ptr([]models.UserID{*q.SubscriberUserID})
}
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_confirmed" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_unconfirmed" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(false))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else {
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
if q.ChannelOwnerUserID != nil {
filter.ChannelOwnerUserID2 = langext.Ptr([]models.UserID{*q.ChannelOwnerUserID})
}
res, err := h.database.ListSubscriptions(ctx, filter)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
jsonres := langext.ArrMap(res, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() })
@@ -152,14 +168,14 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons
}
_, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
clients, err := h.database.ListSubscriptionsByChannel(ctx, u.ChannelID)
clients, err := h.database.ListSubscriptions(ctx, models.SubscriptionFilter{AnyUserID: langext.Ptr(u.UserID), ChannelID: langext.Ptr([]models.ChannelID{u.ChannelID})})
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
@@ -203,7 +219,7 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
}
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
}
if err != nil {
@@ -250,7 +266,7 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
}
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
}
if err != nil {
@@ -414,7 +430,7 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
userid := *ctx.GetPermissionUserID()
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
}
if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
@@ -167,7 +168,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
}
user, err := h.database.GetUser(ctx, u.UserID)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
}
if err != nil {

View File

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

View File

@@ -9,6 +9,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
@@ -28,7 +29,7 @@ func NewCompatHandler(app *logic.Application) CompatHandler {
}
}
// SendMessageCompat swaggerdoc
// SendMessage swaggerdoc
//
// @Deprecated
//
@@ -36,17 +37,17 @@ func NewCompatHandler(app *logic.Application) CompatHandler {
// @Description All parameter can be set via query-parameter or form-data body. Only UserID, UserKey and Title are required
// @Tags External
//
// @Param query_data query handler.SendMessageCompat.combined false " "
// @Param form_data formData handler.SendMessageCompat.combined false " "
// @Param query_data query handler.SendMessage.combined false " "
// @Param form_data formData handler.SendMessage.combined false " "
//
// @Success 200 {object} handler.SendMessageCompat.response
// @Success 200 {object} handler.SendMessage.response
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError
// @Failure 403 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
//
// @Router /send.php [POST]
func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
func (h CompatHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
type combined struct {
UserID *int64 `json:"user_id" form:"user_id"`
UserKey *string `json:"user_key" form:"user_key"`
@@ -87,7 +88,7 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil)
}
okResp, errResp := h.sendMessageInternal(g, ctx, langext.Ptr(models.UserID(*newid)), data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil)
okResp, errResp := h.app.SendMessage(g, ctx, langext.Ptr(models.UserID(*newid)), data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil)
if errResp != nil {
return *errResp
} else {
@@ -287,7 +288,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
}
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(201, "User not found")
}
if err != nil {
@@ -295,7 +296,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
@@ -311,7 +312,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
}
filter := models.MessageFilter{
Owner: langext.Ptr([]models.UserID{user.UserID}),
Sender: langext.Ptr([]models.UserID{user.UserID}),
CompatAcknowledged: langext.Ptr(false),
}
@@ -395,7 +396,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
}
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(201, "User not found")
}
if err != nil {
@@ -403,7 +404,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
@@ -497,7 +498,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
}
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(201, "User not found")
}
if err != nil {
@@ -505,7 +506,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
@@ -516,7 +517,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
}
filter := models.MessageFilter{
Owner: langext.Ptr([]models.UserID{user.UserID}),
Sender: langext.Ptr([]models.UserID{user.UserID}),
CompatAcknowledged: langext.Ptr(false),
}
@@ -528,7 +529,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
compMsgs := make([]models.CompatMessage, 0, len(msgs))
for _, v := range msgs {
messageIdComp, err := h.database.ConvertToCompatIDOrCreate(ctx, v.MessageID.String(), "messageid")
messageIdComp, err := h.database.ConvertToCompatIDOrCreate(ctx, "messageid", v.MessageID.String())
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create messageid<old>", err)
}
@@ -614,7 +615,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
}
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(201, "User not found")
}
if err != nil {
@@ -622,7 +623,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
@@ -744,7 +745,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
}
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(201, "User not found")
}
if err != nil {
@@ -752,7 +753,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
@@ -771,7 +772,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
}
msg, err := h.database.GetMessage(ctx, models.MessageID(*messageCompNew), false)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(301, "Message not found")
}
if err != nil {
@@ -863,7 +864,7 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
}
user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew))
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(201, "User not found")
}
if err != nil {
@@ -871,7 +872,7 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {

View File

@@ -0,0 +1,134 @@
package handler
import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"fmt"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http"
"time"
)
type ExternalHandler struct {
app *logic.Application
database *primarydb.Database
}
func NewExternalHandler(app *logic.Application) ExternalHandler {
return ExternalHandler{
app: app,
database: app.Database.Primary,
}
}
// UptimeKuma swaggerdoc
//
// @Summary Send a new message
// @Description All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required
// @Tags External
//
// @Param query_data query handler.UptimeKuma.query false " "
// @Param post_body body handler.UptimeKuma.body false " "
//
// @Success 200 {object} handler.UptimeKuma.response
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError "The user_id was not found or the user_key is wrong"
// @Failure 403 {object} ginresp.apiError "The user has exceeded its daily quota - wait 24 hours or upgrade your account"
// @Failure 500 {object} ginresp.apiError "An internal server error occurred - try again later"
//
// @Router /external/v1/uptime-kuma [POST]
func (h ExternalHandler) UptimeKuma(g *gin.Context) ginresp.HTTPResponse {
type query struct {
UserID *models.UserID `form:"user_id" example:"7725"`
KeyToken *string `form:"key" example:"P3TNH8mvv14fm"`
Channel *string `form:"channel"`
ChannelUp *string `form:"channel_up"`
ChannelDown *string `form:"channel_down"`
Priority *int `form:"priority"`
PriorityUp *int `form:"priority_up"`
PriorityDown *int `form:"priority_down"`
SenderName *string `form:"senderName"`
}
type body struct {
Heartbeat *struct {
Time string `json:"time"`
Status int `json:"status"`
Msg string `json:"msg"`
Timezone string `json:"timezone"`
TimezoneOffset string `json:"timezoneOffset"`
LocalDateTime string `json:"localDateTime"`
} `json:"heartbeat"`
Monitor *struct {
Name string `json:"name"`
Url *string `json:"url"`
} `json:"monitor"`
Msg *string `json:"msg"`
}
type response struct {
MessageID models.MessageID `json:"message_id"`
}
var b body
var q query
ctx, httpErr := h.app.StartRequest(g, nil, &q, &b, nil)
if httpErr != nil {
return *httpErr
}
defer ctx.Cancel()
if b.Heartbeat == nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'heartbeat' in request body", nil)
}
if b.Monitor == nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'monitor' in request body", nil)
}
if b.Msg == nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'msg' in request body", nil)
}
title := langext.Conditional(b.Heartbeat.Status == 1, fmt.Sprintf("Monitor %v is back online", b.Monitor.Name), fmt.Sprintf("Monitor %v went down!", b.Monitor.Name))
content := b.Heartbeat.Msg
var timestamp *float64 = nil
if tz, err := time.LoadLocation(b.Heartbeat.Timezone); err == nil {
if ts, err := time.ParseInLocation("2006-01-02 15:04:05", b.Heartbeat.LocalDateTime, tz); err == nil {
timestamp = langext.Ptr(float64(ts.Unix()))
}
}
var channel *string = nil
if q.Channel != nil {
channel = q.Channel
}
if q.ChannelUp != nil && b.Heartbeat.Status == 1 {
channel = q.ChannelUp
}
if q.ChannelDown != nil && b.Heartbeat.Status != 1 {
channel = q.ChannelDown
}
var priority *int = nil
if q.Priority != nil {
priority = q.Priority
}
if q.PriorityUp != nil && b.Heartbeat.Status == 1 {
priority = q.PriorityUp
}
if q.PriorityDown != nil && b.Heartbeat.Status != 1 {
priority = q.PriorityDown
}
okResp, errResp := h.app.SendMessage(g, ctx, q.UserID, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName)
if errResp != nil {
return *errResp
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{
MessageID: okResp.Message.MessageID,
}))
}

View File

@@ -2,22 +2,14 @@ package handler
import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"net/http"
"strings"
"time"
)
type SendMessageResponse struct {
@@ -95,7 +87,7 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
// query has highest prio, then form, then json
data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q)
okResp, errResp := h.sendMessageInternal(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName)
okResp, errResp := h.app.SendMessage(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName)
if errResp != nil {
return *errResp
} else {
@@ -113,203 +105,3 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
}))
}
}
func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContext, UserID *models.UserID, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginresp.HTTPResponse) {
if Title != nil {
Title = langext.Ptr(strings.TrimSpace(*Title))
}
if UserMessageID != nil {
UserMessageID = langext.Ptr(strings.TrimSpace(*UserMessageID))
}
if UserID == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_UID, hl.USER_ID, "Missing parameter [[user_id]]", nil))
}
if Key == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[key]]", nil))
}
if Title == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TITLE, hl.TITLE, "Missing parameter [[title]]", nil))
}
if Priority != nil && (*Priority != 0 && *Priority != 1 && *Priority != 2) {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.INVALID_PRIO, hl.PRIORITY, "Invalid priority", nil))
}
if len(*Title) == 0 {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.NO_TITLE, hl.TITLE, "No title specified", nil))
}
user, err := h.database.GetUser(ctx, *UserID)
if err == sql.ErrNoRows {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", err))
}
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err))
}
channelDisplayName := user.DefaultChannel()
channelInternalName := user.DefaultChannel()
if Channel != nil {
channelDisplayName = h.app.NormalizeChannelDisplayName(*Channel)
channelInternalName = h.app.NormalizeChannelInternalName(*Channel)
}
if len(*Title) > user.MaxTitleLength() {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.TITLE_TOO_LONG, hl.TITLE, fmt.Sprintf("Title too long (max %d characters)", user.MaxTitleLength()), nil))
}
if Content != nil && len(*Content) > user.MaxContentLength() {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, hl.CONTENT, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil))
}
if len(channelDisplayName) > user.MaxChannelNameLength() {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil))
}
if len(strings.TrimSpace(channelDisplayName)) == 0 {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_NAME_EMPTY, hl.CHANNEL, fmt.Sprintf("Channel displayname cannot be empty"), nil))
}
if len(channelInternalName) > user.MaxChannelNameLength() {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil))
}
if len(strings.TrimSpace(channelInternalName)) == 0 {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_NAME_EMPTY, hl.CHANNEL, fmt.Sprintf("Channel internalname cannot be empty"), nil))
}
if SenderName != nil && len(*SenderName) > user.MaxSenderName() {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.SENDERNAME_TOO_LONG, hl.SENDER_NAME, fmt.Sprintf("SenderName too long (max %d characters)", user.MaxSenderName()), nil))
}
if UserMessageID != nil && len(*UserMessageID) > user.MaxUserMessageID() {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.USR_MSG_ID_TOO_LONG, hl.USER_MESSAGE_ID, fmt.Sprintf("MessageID too long (max %d characters)", user.MaxUserMessageID()), nil))
}
if SendTimestamp != nil && mathext.Abs(*SendTimestamp-float64(time.Now().Unix())) > timeext.FromHours(user.MaxTimestampDiffHours()).Seconds() {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.TIMESTAMP_OUT_OF_RANGE, hl.NONE, fmt.Sprintf("The timestamp mus be within %d hours of now()", user.MaxTimestampDiffHours()), nil))
}
if UserMessageID != nil {
msg, err := h.database.GetMessageByUserMessageID(ctx, *UserMessageID)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query existing message", err))
}
if msg != nil {
existingCompID, _, err := h.database.ConvertToCompatID(ctx, msg.MessageID.String())
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query compat-id", err))
}
if existingCompID == nil {
v, err := h.database.CreateCompatID(ctx, "messageid", msg.MessageID.String())
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
}
existingCompID = &v
}
//the found message can be deleted (!), but we still return NO_ERROR here...
return &SendMessageResponse{
User: user,
Message: *msg,
MessageIsOld: true,
CompatMessageID: *existingCompID,
}, nil
}
}
if user.QuotaRemainingToday() <= 0 {
return nil, langext.Ptr(ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, hl.NONE, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil))
}
channel, err := h.app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err))
}
keytok, permResp := ctx.CheckPermissionSend(channel, *Key)
if permResp != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, hl.USER_KEY, "You are not authorized for this action", nil))
}
var sendTimestamp *time.Time = nil
if SendTimestamp != nil {
sendTimestamp = langext.Ptr(timeext.UnixFloatSeconds(*SendTimestamp))
}
priority := langext.Coalesce(Priority, user.DefaultPriority())
clientIP := g.ClientIP()
msg, err := h.database.CreateMessage(ctx, *UserID, channel, sendTimestamp, *Title, Content, priority, UserMessageID, clientIP, SenderName, keytok.KeyTokenID)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create message in db", err))
}
compatMsgID, err := h.database.CreateCompatID(ctx, "messageid", msg.MessageID.String())
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
}
subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query subscriptions", err))
}
err = h.database.IncUserMessageCounter(ctx, &user)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc user msg-counter", err))
}
err = h.database.IncChannelMessageCounter(ctx, &channel)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc channel msg-counter", err))
}
err = h.database.IncKeyTokenMessageCounter(ctx, keytok)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc token msg-counter", err))
}
log.Info().Msg(fmt.Sprintf("Sending new notification %s for user %s", msg.MessageID, UserID))
for _, sub := range subscriptions {
clients, err := h.database.ListClients(ctx, sub.SubscriberUserID)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query clients", err))
}
if !sub.Confirmed {
continue
}
for _, client := range clients {
isCompatClient, err := h.database.IsCompatClient(ctx, client.ClientID)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query compat_clients", err))
}
var titleOverride *string = nil
var msgidOverride *string = nil
if isCompatClient {
titleOverride = langext.Ptr(h.app.CompatizeMessageTitle(ctx, msg))
msgidOverride = langext.Ptr(fmt.Sprintf("%d", compatMsgID))
}
fcmDelivID, err := h.app.DeliverMessage(ctx, client, msg, titleOverride, msgidOverride)
if err != nil {
_, err = h.database.CreateRetryDelivery(ctx, client, msg)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create delivery", err))
}
} else {
_, err = h.database.CreateSuccessDelivery(ctx, client, msg, fcmDelivID)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create delivery", err))
}
}
}
}
return &SendMessageResponse{
User: user,
Message: msg,
MessageIsOld: false,
CompatMessageID: compatMsgID,
}, nil
}

View File

@@ -16,22 +16,24 @@ import (
type Router struct {
app *logic.Application
commonHandler handler.CommonHandler
compatHandler handler.CompatHandler
websiteHandler handler.WebsiteHandler
apiHandler handler.APIHandler
messageHandler handler.MessageHandler
commonHandler handler.CommonHandler
compatHandler handler.CompatHandler
websiteHandler handler.WebsiteHandler
apiHandler handler.APIHandler
messageHandler handler.MessageHandler
externalHandler handler.ExternalHandler
}
func NewRouter(app *logic.Application) *Router {
return &Router{
app: app,
commonHandler: handler.NewCommonHandler(app),
compatHandler: handler.NewCompatHandler(app),
websiteHandler: handler.NewWebsiteHandler(app),
apiHandler: handler.NewAPIHandler(app),
messageHandler: handler.NewMessageHandler(app),
commonHandler: handler.NewCommonHandler(app),
compatHandler: handler.NewCompatHandler(app),
websiteHandler: handler.NewWebsiteHandler(app),
apiHandler: handler.NewAPIHandler(app),
messageHandler: handler.NewMessageHandler(app),
externalHandler: handler.NewExternalHandler(app),
}
}
@@ -122,7 +124,6 @@ func (r *Router) Init(e *gin.Engine) error {
apiv2 := e.Group("/api/v2/")
{
apiv2.POST("/users", r.Wrap(r.apiHandler.CreateUser))
apiv2.GET("/users/:uid", r.Wrap(r.apiHandler.GetUser))
apiv2.PATCH("/users/:uid", r.Wrap(r.apiHandler.UpdateUser))
@@ -163,7 +164,10 @@ func (r *Router) Init(e *gin.Engine) error {
{
sendAPI.POST("/", r.Wrap(r.messageHandler.SendMessage))
sendAPI.POST("/send", r.Wrap(r.messageHandler.SendMessage))
sendAPI.POST("/send.php", r.Wrap(r.messageHandler.SendMessageCompat))
sendAPI.POST("/send.php", r.Wrap(r.compatHandler.SendMessage))
sendAPI.POST("/external/v1/uptime-kuma", r.Wrap(r.externalHandler.UptimeKuma))
}
// ================

View File

@@ -36,6 +36,13 @@ func main() {
}
fmt.Printf("PrimarySchema3 := %s\n", h0)
}
{
h0, err := sq.HashSqliteSchema(ctx, schema.PrimarySchema4)
if err != nil {
h0 = "ERR"
}
fmt.Printf("PrimarySchema4 := %s\n", h0)
}
{
h0, err := sq.HashSqliteSchema(ctx, schema.RequestsSchema1)
if err != nil {

View File

@@ -1,7 +1,6 @@
package primary
package db
import (
"blackforestbytes.com/simplecloudnotifier/db"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
)
@@ -12,5 +11,5 @@ type TxContext interface {
Err() error
Value(key any) any
GetOrCreateTransaction(db db.DatabaseImpl) (sq.Tx, error)
GetOrCreateTransaction(db DatabaseImpl) (sq.Tx, error)
}

View File

@@ -13,17 +13,17 @@ type DatabaseImpl interface {
BeginTx(ctx context.Context) (sq.Tx, error)
Stop(ctx context.Context) error
ReadSchema(ctx context.Context) (int, error)
ReadSchema(ctx TxContext) (int, error)
WriteMetaString(ctx context.Context, key string, value string) error
WriteMetaInt(ctx context.Context, key string, value int64) error
WriteMetaReal(ctx context.Context, key string, value float64) error
WriteMetaBlob(ctx context.Context, key string, value []byte) error
WriteMetaString(ctx TxContext, key string, value string) error
WriteMetaInt(ctx TxContext, key string, value int64) error
WriteMetaReal(ctx TxContext, key string, value float64) error
WriteMetaBlob(ctx TxContext, key string, value []byte) error
ReadMetaString(ctx context.Context, key string) (*string, error)
ReadMetaInt(ctx context.Context, key string) (*int64, error)
ReadMetaReal(ctx context.Context, key string) (*float64, error)
ReadMetaBlob(ctx context.Context, key string) (*[]byte, error)
ReadMetaString(ctx TxContext, key string) (*string, error)
ReadMetaInt(ctx TxContext, key string) (*int64, error)
ReadMetaReal(ctx TxContext, key string) (*float64, error)
ReadMetaBlob(ctx TxContext, key string) (*[]byte, error)
DeleteMeta(ctx context.Context, key string) error
DeleteMeta(ctx TxContext, key string) error
}

View File

@@ -4,6 +4,7 @@ import (
server "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db/dbtools"
"blackforestbytes.com/simplecloudnotifier/db/schema"
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"context"
"database/sql"
"errors"
@@ -63,77 +64,93 @@ func (db *Database) DB() sq.DB {
return db.db
}
func (db *Database) Migrate(ctx context.Context) error {
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second)
defer cancel()
func (db *Database) Migrate(outerctx context.Context) error {
innerctx, cancel := context.WithTimeout(outerctx, 24*time.Second)
tctx := simplectx.CreateSimpleContext(innerctx, cancel)
currschema, err := db.ReadSchema(ctx)
tx, err := tctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
defer func() {
if tx.Status() == sq.TxStatusInitial || tx.Status() == sq.TxStatusActive {
err = tx.Rollback()
if err != nil {
log.Err(err).Msg("failed to rollback transaction")
}
}
}()
ppReInit := false
currschema, err := db.ReadSchema(tctx)
if err != nil {
return err
}
if currschema == 0 {
schemastr := schema.LogsSchema[schema.LogsSchemaVersion].SQL
schemahash := schema.LogsSchema[schema.LogsSchemaVersion].Hash
schemastr := schema.LogsSchema1
schemahash, err := sq.HashSqliteSchema(ctx, schemastr)
_, err = tx.Exec(tctx, schemastr, sq.PP{})
if err != nil {
return err
}
_, err = db.db.Exec(ctx, schemastr, sq.PP{})
err = db.WriteMetaInt(tctx, "schema", int64(schema.LogsSchemaVersion))
if err != nil {
return err
}
err = db.WriteMetaInt(ctx, "schema", 1)
err = db.WriteMetaString(tctx, "schema_hash", schemahash)
if err != nil {
return err
}
err = db.WriteMetaString(ctx, "schema_hash", schemahash)
ppReInit = true
currschema = schema.LogsSchemaVersion
}
if currschema == 1 {
schemHashDB, err := sq.HashSqliteDatabase(tctx, tx)
if err != nil {
return err
}
err = db.pp.Init(ctx) // Re-Init
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
if err != nil {
return err
}
return nil
} else if currschema == 1 {
schemHashDB, err := sq.HashSqliteDatabase(ctx, db.db)
if err != nil {
return err
}
schemaHashMeta, err := db.ReadMetaString(ctx, "schema_hash")
if err != nil {
return err
}
schemHashAsset := schema.LogsHash1
if err != nil {
return err
}
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schemHashAsset {
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.LogsSchema[currschema].Hash {
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (logs db)")
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).Msg("Schema (logs db)")
log.Debug().Str("schemHashAsset", schemHashAsset).Msg("Schema (logs db)")
log.Debug().Str("schemaHashAsset", schema.LogsSchema[currschema].Hash).Msg("Schema (logs db)")
return errors.New("database schema does not match (logs db)")
} else {
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (logs db)")
}
}
return nil // current
} else {
if currschema != schema.LogsSchemaVersion {
return errors.New(fmt.Sprintf("Unknown DB schema: %d", currschema))
}
err = tx.Commit()
if err != nil {
return err
}
if ppReInit {
log.Debug().Msg("Re-Init preprocessor")
err = db.pp.Init(outerctx) // Re-Init
if err != nil {
return err
}
}
return nil
}
func (db *Database) Ping(ctx context.Context) error {

View File

@@ -1,15 +1,19 @@
package logs
import (
"context"
"blackforestbytes.com/simplecloudnotifier/db"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
)
func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
func (db *Database) ReadSchema(ctx db.TxContext) (retval int, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return 0, err
}
r1, err := db.db.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
r1, err := tx.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
if err != nil {
return 0, err
}
@@ -31,7 +35,7 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return 0, err
}
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
if err != nil {
return 0, err
}
@@ -62,8 +66,13 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return dbschema, nil
}
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
func (db *Database) WriteMetaString(ctx db.TxContext, key string, value string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
"key": key,
"val": value,
})
@@ -73,8 +82,13 @@ func (db *Database) WriteMetaString(ctx context.Context, key string, value strin
return nil
}
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
func (db *Database) WriteMetaInt(ctx db.TxContext, key string, value int64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
"key": key,
"val": value,
})
@@ -84,8 +98,13 @@ func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) e
return nil
}
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
func (db *Database) WriteMetaReal(ctx db.TxContext, key string, value float64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
"key": key,
"val": value,
})
@@ -95,8 +114,13 @@ func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64
return nil
}
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
func (db *Database) WriteMetaBlob(ctx db.TxContext, key string, value []byte) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
"key": key,
"val": value,
})
@@ -106,8 +130,13 @@ func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte)
return nil
}
func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *string, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaString(ctx db.TxContext, key string) (retval *string, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -137,8 +166,13 @@ func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *str
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaInt(ctx db.TxContext, key string) (retval *int64, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -169,8 +203,13 @@ func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64,
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float64, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaReal(ctx db.TxContext, key string) (retval *float64, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -201,8 +240,13 @@ func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byte, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaBlob(ctx db.TxContext, key string) (retval *[]byte, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -233,8 +277,13 @@ func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byt
return langext.Ptr(value), nil
}
func (db *Database) DeleteMeta(ctx context.Context, key string) error {
_, err := db.db.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) DeleteMeta(ctx db.TxContext, key string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return err
}

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import (
server "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db/dbtools"
"blackforestbytes.com/simplecloudnotifier/db/schema"
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"context"
"database/sql"
"errors"
@@ -63,81 +64,147 @@ func (db *Database) DB() sq.DB {
return db.db
}
func (db *Database) Migrate(ctx context.Context) error {
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second)
defer cancel()
func (db *Database) Migrate(outerctx context.Context) error {
innerctx, cancel := context.WithTimeout(outerctx, 24*time.Second)
tctx := simplectx.CreateSimpleContext(innerctx, cancel)
currschema, err := db.ReadSchema(ctx)
tx, err := tctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
defer func() {
if tx.Status() == sq.TxStatusInitial || tx.Status() == sq.TxStatusActive {
err = tx.Rollback()
if err != nil {
log.Err(err).Msg("failed to rollback transaction")
}
}
}()
ppReInit := false
currschema, err := db.ReadSchema(tctx)
if err != nil {
return err
}
if currschema == 0 {
schemastr := schema.PrimarySchema[schema.PrimarySchemaVersion].SQL
schemahash := schema.PrimarySchema[schema.PrimarySchemaVersion].Hash
schemastr := schema.PrimarySchema3
schemahash, err := sq.HashSqliteSchema(ctx, schemastr)
_, err = tx.Exec(tctx, schemastr, sq.PP{})
if err != nil {
return err
}
_, err = db.db.Exec(ctx, schemastr, sq.PP{})
err = db.WriteMetaInt(tctx, "schema", int64(schema.PrimarySchemaVersion))
if err != nil {
return err
}
err = db.WriteMetaInt(ctx, "schema", 3)
err = db.WriteMetaString(tctx, "schema_hash", schemahash)
if err != nil {
return err
}
err = db.WriteMetaString(ctx, "schema_hash", schemahash)
if err != nil {
return err
}
ppReInit = true
err = db.pp.Init(ctx) // Re-Init
if err != nil {
return err
}
currschema = schema.PrimarySchemaVersion
}
return nil
} else if currschema == 1 {
if currschema == 1 {
return errors.New("cannot autom. upgrade schema 1")
} else if currschema == 2 {
}
if currschema == 2 {
return errors.New("cannot autom. upgrade schema 2")
} else if currschema == 3 {
}
schemHashDB, err := sq.HashSqliteDatabase(ctx, db.db)
if currschema == 3 {
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
if err != nil {
return err
}
schemaHashMeta, err := db.ReadMetaString(ctx, "schema_hash")
schemHashDB, err := sq.HashSqliteDatabase(tctx, tx)
if err != nil {
return err
}
schemHashAsset := schema.PrimaryHash3
if err != nil {
return err
}
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schemHashAsset {
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.PrimarySchema[currschema].Hash {
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (primary db)")
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).Msg("Schema (primary db)")
log.Debug().Str("schemHashAsset", schemHashAsset).Msg("Schema (primary db)")
log.Debug().Str("schemaHashAsset", schema.PrimarySchema[currschema].Hash).Msg("Schema (primary db)")
return errors.New("database schema does not match (primary db)")
} else {
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (primary db)")
}
return nil // current
} else {
log.Info().Int("currschema", currschema).Msg("Upgrade schema from 3 -> 4")
_, err = tx.Exec(tctx, schema.PrimaryMigration_3_4, sq.PP{})
if err != nil {
return err
}
currschema = 4
err = db.WriteMetaInt(tctx, "schema", int64(currschema))
if err != nil {
return err
}
err = db.WriteMetaString(tctx, "schema_hash", schema.PrimarySchema[currschema].Hash)
if err != nil {
return err
}
log.Info().Int("currschema", currschema).Msg("Upgrade schema from 3 -> 4 succesfuly")
ppReInit = true
}
if currschema == 4 {
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
if err != nil {
return err
}
schemHashDB, err := sq.HashSqliteDatabase(tctx, tx)
if err != nil {
return err
}
if schemHashDB != langext.Coalesce(schemaHashMeta, "") || langext.Coalesce(schemaHashMeta, "") != schema.PrimarySchema[currschema].Hash {
log.Debug().Str("schemHashDB", schemHashDB).Msg("Schema (primary db)")
log.Debug().Str("schemaHashMeta", langext.Coalesce(schemaHashMeta, "")).Msg("Schema (primary db)")
log.Debug().Str("schemaHashAsset", schema.PrimarySchema[currschema].Hash).Msg("Schema (primary db)")
return errors.New("database schema does not match (primary db)")
} else {
log.Debug().Str("schemHash", schemHashDB).Msg("Verified Schema consistency (primary db)")
}
}
if currschema != schema.PrimarySchemaVersion {
return errors.New(fmt.Sprintf("Unknown DB schema: %d", currschema))
}
err = tx.Commit()
if err != nil {
return err
}
if ppReInit {
log.Debug().Msg("Re-Init preprocessor")
err = db.pp.Init(outerctx) // Re-Init
if err != nil {
return err
}
}
return nil
}
func (db *Database) Ping(ctx context.Context) error {

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package primary
import (
"blackforestbytes.com/simplecloudnotifier/db"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
@@ -9,7 +10,7 @@ import (
"time"
)
func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*models.Message, error) {
func (db *Database) GetMessageByUserMessageID(ctx db.TxContext, usrMsgId string) (*models.Message, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
@@ -21,7 +22,7 @@ func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*
}
msg, err := models.DecodeMessage(rows)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
@@ -31,7 +32,7 @@ func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*
return &msg, nil
}
func (db *Database) GetMessage(ctx TxContext, scnMessageID models.MessageID, allowDeleted bool) (models.Message, error) {
func (db *Database) GetMessage(ctx db.TxContext, scnMessageID models.MessageID, allowDeleted bool) (models.Message, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Message{}, err
@@ -57,7 +58,7 @@ func (db *Database) GetMessage(ctx TxContext, scnMessageID models.MessageID, all
return msg, nil
}
func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, channel models.Channel, timestampSend *time.Time, title string, content *string, priority int, userMsgId *string, senderIP string, senderName *string, usedKeyID models.KeyTokenID) (models.Message, error) {
func (db *Database) CreateMessage(ctx db.TxContext, senderUserID models.UserID, channel models.Channel, timestampSend *time.Time, title string, content *string, priority int, userMsgId *string, senderIP string, senderName *string, usedKeyID models.KeyTokenID) (models.Message, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Message{}, err
@@ -66,7 +67,6 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
entity := models.MessageDB{
MessageID: models.NewMessageID(),
SenderUserID: senderUserID,
OwnerUserID: channel.OwnerUserID,
ChannelInternalName: channel.InternalName,
ChannelID: channel.ChannelID,
SenderIP: senderIP,
@@ -89,7 +89,7 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
return entity.Model(), nil
}
func (db *Database) DeleteMessage(ctx TxContext, messageID models.MessageID) error {
func (db *Database) DeleteMessage(ctx db.TxContext, messageID models.MessageID) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
@@ -103,7 +103,7 @@ func (db *Database) DeleteMessage(ctx TxContext, messageID models.MessageID) err
return nil
}
func (db *Database) ListMessages(ctx TxContext, filter models.MessageFilter, pageSize *int, inTok ct.CursorToken) ([]models.Message, ct.CursorToken, error) {
func (db *Database) ListMessages(ctx db.TxContext, filter models.MessageFilter, pageSize *int, inTok ct.CursorToken) ([]models.Message, ct.CursorToken, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, ct.CursorToken{}, err
@@ -151,7 +151,7 @@ func (db *Database) ListMessages(ctx TxContext, filter models.MessageFilter, pag
}
}
func (db *Database) CountMessages(ctx TxContext, filter models.MessageFilter) (int64, error) {
func (db *Database) CountMessages(ctx db.TxContext, filter models.MessageFilter) (int64, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return 0, err

View File

@@ -1,15 +1,19 @@
package primary
import (
"context"
"blackforestbytes.com/simplecloudnotifier/db"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
)
func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
func (db *Database) ReadSchema(ctx db.TxContext) (retval int, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return 0, err
}
r1, err := db.db.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
r1, err := tx.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
if err != nil {
return 0, err
}
@@ -31,7 +35,7 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return 0, err
}
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
if err != nil {
return 0, err
}
@@ -62,8 +66,13 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return dbschema, nil
}
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
func (db *Database) WriteMetaString(ctx db.TxContext, key string, value string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
"key": key,
"val": value,
})
@@ -73,8 +82,13 @@ func (db *Database) WriteMetaString(ctx context.Context, key string, value strin
return nil
}
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
func (db *Database) WriteMetaInt(ctx db.TxContext, key string, value int64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
"key": key,
"val": value,
})
@@ -84,8 +98,13 @@ func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) e
return nil
}
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
func (db *Database) WriteMetaReal(ctx db.TxContext, key string, value float64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
"key": key,
"val": value,
})
@@ -95,8 +114,13 @@ func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64
return nil
}
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
func (db *Database) WriteMetaBlob(ctx db.TxContext, key string, value []byte) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
"key": key,
"val": value,
})
@@ -106,8 +130,13 @@ func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte)
return nil
}
func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *string, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaString(ctx db.TxContext, key string) (retval *string, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -137,8 +166,13 @@ func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *str
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaInt(ctx db.TxContext, key string) (retval *int64, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -169,8 +203,13 @@ func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64,
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float64, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaReal(ctx db.TxContext, key string) (retval *float64, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -201,8 +240,13 @@ func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byte, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaBlob(ctx db.TxContext, key string) (retval *[]byte, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -233,8 +277,13 @@ func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byt
return langext.Ptr(value), nil
}
func (db *Database) DeleteMeta(ctx context.Context, key string) error {
_, err := db.db.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) DeleteMeta(ctx db.TxContext, key string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return err
}

View File

@@ -1,13 +1,15 @@
package primary
import (
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
)
func (db *Database) CreateSubscription(ctx TxContext, subscriberUID models.UserID, channel models.Channel, confirmed bool) (models.Subscription, error) {
func (db *Database) CreateSubscription(ctx db.TxContext, subscriberUID models.UserID, channel models.Channel, confirmed bool) (models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Subscription{}, err
@@ -31,15 +33,19 @@ func (db *Database) CreateSubscription(ctx TxContext, subscriberUID models.UserI
return entity.Model(), nil
}
func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID models.ChannelID) ([]models.Subscription, error) {
func (db *Database) ListSubscriptions(ctx db.TxContext, filter models.SubscriptionFilter) ([]models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
order := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC "
filterCond, filterJoin, prepParams, err := filter.SQL()
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE channel_id = :cid"+order, sq.PP{"cid": channelID})
orderClause := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC "
sqlQuery := "SELECT " + "subscriptions.*" + " FROM subscriptions " + filterJoin + " WHERE ( " + filterCond + " ) " + orderClause
rows, err := tx.Query(ctx, sqlQuery, prepParams)
if err != nil {
return nil, err
}
@@ -52,63 +58,7 @@ func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID models.C
return data, nil
}
func (db *Database) ListSubscriptionsByChannelOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
cond := ""
if confirmed != nil && *confirmed {
cond = " AND confirmed = 1"
} else if confirmed != nil && !*confirmed {
cond = " AND confirmed = 0"
}
order := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC "
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE channel_owner_user_id = :ouid"+cond+order, sq.PP{"ouid": ownerUserID})
if err != nil {
return nil, err
}
data, err := models.DecodeSubscriptions(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) ListSubscriptionsBySubscriber(ctx TxContext, subscriberUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
cond := ""
if confirmed != nil && *confirmed {
cond = " AND confirmed = 1"
} else if confirmed != nil && !*confirmed {
cond = " AND confirmed = 0"
}
order := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC "
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE subscriber_user_id = :suid"+cond+order, sq.PP{"suid": subscriberUserID})
if err != nil {
return nil, err
}
data, err := models.DecodeSubscriptions(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) GetSubscription(ctx TxContext, subid models.SubscriptionID) (models.Subscription, error) {
func (db *Database) GetSubscription(ctx db.TxContext, subid models.SubscriptionID) (models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Subscription{}, err
@@ -127,7 +77,7 @@ func (db *Database) GetSubscription(ctx TxContext, subid models.SubscriptionID)
return sub, nil
}
func (db *Database) GetSubscriptionBySubscriber(ctx TxContext, subscriberId models.UserID, channelId models.ChannelID) (*models.Subscription, error) {
func (db *Database) GetSubscriptionBySubscriber(ctx db.TxContext, subscriberId models.UserID, channelId models.ChannelID) (*models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
@@ -142,7 +92,7 @@ func (db *Database) GetSubscriptionBySubscriber(ctx TxContext, subscriberId mode
}
user, err := models.DecodeSubscription(rows)
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
@@ -152,7 +102,7 @@ func (db *Database) GetSubscriptionBySubscriber(ctx TxContext, subscriberId mode
return &user, nil
}
func (db *Database) DeleteSubscription(ctx TxContext, subid models.SubscriptionID) error {
func (db *Database) DeleteSubscription(ctx db.TxContext, subid models.SubscriptionID) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
@@ -166,7 +116,7 @@ func (db *Database) DeleteSubscription(ctx TxContext, subid models.SubscriptionI
return nil
}
func (db *Database) UpdateSubscriptionConfirmed(ctx TxContext, subscriptionID models.SubscriptionID, confirmed bool) error {
func (db *Database) UpdateSubscriptionConfirmed(ctx db.TxContext, subscriptionID models.SubscriptionID, confirmed bool) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err

View File

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

View File

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

View File

@@ -1,15 +1,19 @@
package requests
import (
"context"
"blackforestbytes.com/simplecloudnotifier/db"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
)
func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
func (db *Database) ReadSchema(ctx db.TxContext) (retval int, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return 0, err
}
r1, err := db.db.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
r1, err := tx.Query(ctx, "SELECT name FROM sqlite_master WHERE type = :typ AND name = :name", sq.PP{"typ": "table", "name": "meta"})
if err != nil {
return 0, err
}
@@ -31,7 +35,7 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return 0, err
}
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": "schema"})
if err != nil {
return 0, err
}
@@ -62,8 +66,13 @@ func (db *Database) ReadSchema(ctx context.Context) (retval int, reterr error) {
return dbschema, nil
}
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
func (db *Database) WriteMetaString(ctx db.TxContext, key string, value string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
"key": key,
"val": value,
})
@@ -73,8 +82,13 @@ func (db *Database) WriteMetaString(ctx context.Context, key string, value strin
return nil
}
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
func (db *Database) WriteMetaInt(ctx db.TxContext, key string, value int64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
"key": key,
"val": value,
})
@@ -84,8 +98,13 @@ func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) e
return nil
}
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
func (db *Database) WriteMetaReal(ctx db.TxContext, key string, value float64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_real = :val", sq.PP{
"key": key,
"val": value,
})
@@ -95,8 +114,13 @@ func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64
return nil
}
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error {
_, err := db.db.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
func (db *Database) WriteMetaBlob(ctx db.TxContext, key string, value []byte) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_blob = :val", sq.PP{
"key": key,
"val": value,
})
@@ -106,8 +130,13 @@ func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte)
return nil
}
func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *string, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaString(ctx db.TxContext, key string) (retval *string, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_txt FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -137,8 +166,13 @@ func (db *Database) ReadMetaString(ctx context.Context, key string) (retval *str
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaInt(ctx db.TxContext, key string) (retval *int64, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_int FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -169,8 +203,13 @@ func (db *Database) ReadMetaInt(ctx context.Context, key string) (retval *int64,
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float64, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaReal(ctx db.TxContext, key string) (retval *float64, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_real FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -201,8 +240,13 @@ func (db *Database) ReadMetaReal(ctx context.Context, key string) (retval *float
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byte, reterr error) {
r2, err := db.db.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) ReadMetaBlob(ctx db.TxContext, key string) (retval *[]byte, reterr error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
r2, err := tx.Query(ctx, "SELECT value_blob FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return nil, err
}
@@ -233,8 +277,13 @@ func (db *Database) ReadMetaBlob(ctx context.Context, key string) (retval *[]byt
return langext.Ptr(value), nil
}
func (db *Database) DeleteMeta(ctx context.Context, key string) error {
_, err := db.db.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
func (db *Database) DeleteMeta(ctx db.TxContext, key string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "DELETE FROM meta WHERE meta_key = :key", sq.PP{"key": key})
if err != nil {
return err
}

View File

@@ -2,27 +2,52 @@ package schema
import _ "embed"
//go:embed primary_1.ddl
var PrimarySchema1 string
type Def struct {
SQL string
Hash string
}
const PrimaryHash1 = "f2b2847f32681a7178e405553beea4a324034915a0c5a5dc70b3c6abbcc852f2"
//go:embed primary_1.ddl
var primarySchema1 string
//go:embed primary_2.ddl
var PrimarySchema2 string
const PrimaryHash2 = "07ed1449114416ed043084a30e0722a5f97bf172161338d2f7106a8dfd387d0a"
var primarySchema2 string
//go:embed primary_3.ddl
var PrimarySchema3 string
var primarySchema3 string
const PrimaryHash3 = "65c2125ad0e12d02490cf2275f0067ef3c62a8522edf9a35ee8aa3f3c09b12e8"
//go:embed primary_4.ddl
var primarySchema4 string
//go:embed primary_migration_3_4.ddl
var PrimaryMigration_3_4 string
//go:embed requests_1.ddl
var RequestsSchema1 string
const RequestsHash1 = "ebb0a5748b605e8215437413b738279670190ca8159b6227cfc2aa13418b41e9"
var requestsSchema1 string
//go:embed logs_1.ddl
var LogsSchema1 string
var logsSchema1 string
const LogsHash1 = "65fba477c04095effc3a8e1bb79fe7547b8e52e983f776f156266eddc4f201d7"
var PrimarySchema = map[int]Def{
0: {"", ""},
1: {primarySchema1, "f2b2847f32681a7178e405553beea4a324034915a0c5a5dc70b3c6abbcc852f2"},
2: {primarySchema2, "07ed1449114416ed043084a30e0722a5f97bf172161338d2f7106a8dfd387d0a"},
3: {primarySchema3, "65c2125ad0e12d02490cf2275f0067ef3c62a8522edf9a35ee8aa3f3c09b12e8"},
4: {primarySchema4, "cb022156ab0e7aea39dd0c985428c43cae7d60e41ca8e9e5a84c774b3019d2ca"},
}
var PrimarySchemaVersion = 4
var RequestsSchema = map[int]Def{
0: {"", ""},
1: {requestsSchema1, "ebb0a5748b605e8215437413b738279670190ca8159b6227cfc2aa13418b41e9"},
}
var RequestsSchemaVersion = 1
var LogsSchema = map[int]Def{
0: {"", ""},
1: {logsSchema1, "65fba477c04095effc3a8e1bb79fe7547b8e52e983f776f156266eddc4f201d7"},
}
var LogsSchemaVersion = 1

View File

@@ -0,0 +1,233 @@
CREATE TABLE users
(
user_id TEXT NOT NULL,
username TEXT NULL DEFAULT NULL,
timestamp_created INTEGER NOT NULL,
timestamp_lastread INTEGER NULL DEFAULT NULL,
timestamp_lastsent INTEGER NULL DEFAULT NULL,
messages_sent INTEGER NOT NULL DEFAULT '0',
quota_used INTEGER NOT NULL DEFAULT '0',
quota_used_day TEXT NULL DEFAULT NULL,
is_pro INTEGER CHECK(is_pro IN (0, 1)) NOT NULL DEFAULT 0,
pro_token TEXT NULL DEFAULT NULL,
PRIMARY KEY (user_id)
) STRICT;
CREATE UNIQUE INDEX "idx_users_protoken" ON users (pro_token) WHERE pro_token IS NOT NULL;
CREATE TABLE keytokens
(
keytoken_id TEXT NOT NULL,
timestamp_created INTEGER NOT NULL,
timestamp_lastused INTEGER NULL DEFAULT NULL,
name TEXT NOT NULL,
owner_user_id TEXT NOT NULL,
all_channels INTEGER CHECK(all_channels IN (0, 1)) NOT NULL,
channels TEXT NOT NULL,
token TEXT NOT NULL,
permissions TEXT NOT NULL,
messages_sent INTEGER NOT NULL DEFAULT '0',
PRIMARY KEY (keytoken_id)
) STRICT;
CREATE UNIQUE INDEX "idx_keytokens_token" ON keytokens (token);
CREATE TABLE clients
(
client_id TEXT NOT NULL,
user_id TEXT NOT NULL,
type TEXT CHECK(type IN ('ANDROID', 'IOS')) NOT NULL,
fcm_token TEXT NOT NULL,
timestamp_created INTEGER NOT NULL,
agent_model TEXT NOT NULL,
agent_version TEXT NOT NULL,
PRIMARY KEY (client_id)
) STRICT;
CREATE INDEX "idx_clients_userid" ON clients (user_id);
CREATE UNIQUE INDEX "idx_clients_fcmtoken" ON clients (fcm_token);
CREATE TABLE channels
(
channel_id TEXT NOT NULL,
owner_user_id TEXT NOT NULL,
internal_name TEXT NOT NULL,
display_name TEXT NOT NULL,
description_name TEXT NULL,
subscribe_key TEXT NOT NULL,
timestamp_created INTEGER NOT NULL,
timestamp_lastsent INTEGER NULL DEFAULT NULL,
messages_sent INTEGER NOT NULL DEFAULT '0',
PRIMARY KEY (channel_id)
) STRICT;
CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, internal_name);
CREATE TABLE subscriptions
(
subscription_id TEXT NOT NULL,
subscriber_user_id TEXT NOT NULL,
channel_owner_user_id TEXT NOT NULL,
channel_internal_name TEXT NOT NULL,
channel_id TEXT NOT NULL,
timestamp_created INTEGER NOT NULL,
confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL,
PRIMARY KEY (subscription_id)
) STRICT;
CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_internal_name);
CREATE INDEX "idx_subscriptions_chan" ON subscriptions (channel_id);
CREATE INDEX "idx_subscriptions_subuser" ON subscriptions (subscriber_user_id);
CREATE INDEX "idx_subscriptions_ownuser" ON subscriptions (channel_owner_user_id);
CREATE INDEX "idx_subscriptions_tsc" ON subscriptions (timestamp_created);
CREATE INDEX "idx_subscriptions_conf" ON subscriptions (confirmed);
CREATE TABLE messages
(
message_id TEXT NOT NULL,
sender_user_id TEXT NOT NULL,
channel_internal_name TEXT NOT NULL,
channel_id TEXT NOT NULL,
sender_ip TEXT NOT NULL,
sender_name TEXT NULL,
timestamp_real INTEGER NOT NULL,
timestamp_client INTEGER NULL,
title TEXT NOT NULL,
content TEXT NULL,
priority INTEGER CHECK(priority IN (0, 1, 2)) NOT NULL,
usr_message_id TEXT NULL,
used_key_id TEXT NOT NULL,
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
PRIMARY KEY (message_id)
) STRICT;
CREATE INDEX "idx_messages_channel" ON messages (channel_internal_name COLLATE BINARY);
CREATE INDEX "idx_messages_channel_nc" ON messages (channel_internal_name COLLATE NOCASE);
CREATE UNIQUE INDEX "idx_messages_idempotency" ON messages (sender_user_id, usr_message_id COLLATE BINARY);
CREATE INDEX "idx_messages_senderip" ON messages (sender_ip COLLATE BINARY);
CREATE INDEX "idx_messages_sendername" ON messages (sender_name COLLATE BINARY);
CREATE INDEX "idx_messages_sendername_nc" ON messages (sender_name COLLATE NOCASE);
CREATE INDEX "idx_messages_title" ON messages (title COLLATE BINARY);
CREATE INDEX "idx_messages_title_nc" ON messages (title COLLATE NOCASE);
CREATE INDEX "idx_messages_usedkey" ON messages (sender_user_id, used_key_id);
CREATE INDEX "idx_messages_deleted" ON messages (deleted);
CREATE VIRTUAL TABLE messages_fts USING fts5
(
channel_internal_name,
sender_name,
title,
content,
tokenize = unicode61,
content = 'messages',
content_rowid = 'rowid'
);
CREATE TRIGGER fts_insert AFTER INSERT ON messages BEGIN
INSERT INTO messages_fts (rowid, channel_internal_name, sender_name, title, content) VALUES (new.rowid, new.channel_internal_name, new.sender_name, new.title, new.content);
END;
CREATE TRIGGER fts_update AFTER UPDATE ON messages BEGIN
INSERT INTO messages_fts (messages_fts, rowid, channel_internal_name, sender_name, title, content) VALUES ('delete', old.rowid, old.channel_internal_name, old.sender_name, old.title, old.content);
INSERT INTO messages_fts ( rowid, channel_internal_name, sender_name, title, content) VALUES ( new.rowid, new.channel_internal_name, new.sender_name, new.title, new.content);
END;
CREATE TRIGGER fts_delete AFTER DELETE ON messages BEGIN
INSERT INTO messages_fts (messages_fts, rowid, channel_internal_name, sender_name, title, content) VALUES ('delete', old.rowid, old.channel_internal_name, old.sender_name, old.title, old.content);
END;
CREATE TABLE deliveries
(
delivery_id TEXT NOT NULL,
message_id TEXT NOT NULL,
receiver_user_id TEXT NOT NULL,
receiver_client_id TEXT NOT NULL,
timestamp_created INTEGER NOT NULL,
timestamp_finalized INTEGER NULL,
status TEXT CHECK(status IN ('RETRY','SUCCESS','FAILED')) NOT NULL,
retry_count INTEGER NOT NULL DEFAULT 0,
next_delivery TEXT NULL DEFAULT NULL,
fcm_message_id TEXT NULL,
PRIMARY KEY (delivery_id)
) STRICT;
CREATE INDEX "idx_deliveries_receiver" ON deliveries (message_id, receiver_client_id);
CREATE TABLE compat_ids
(
old INTEGER NOT NULL,
new TEXT NOT NULL,
type TEXT NOT NULL
) STRICT;
CREATE UNIQUE INDEX "idx_compatids_new" ON compat_ids (new);
CREATE UNIQUE INDEX "idx_compatids_old" ON compat_ids (old, type);
CREATE TABLE compat_acks
(
user_id TEXT NOT NULL,
message_id TEXT NOT NULL
) STRICT;
CREATE INDEX "idx_compatacks_userid" ON compat_acks (user_id);
CREATE UNIQUE INDEX "idx_compatacks_messageid" ON compat_acks (message_id);
CREATE UNIQUE INDEX "idx_compatacks_userid_messageid" ON compat_acks (user_id, message_id);
CREATE TABLE compat_clients
(
client_id TEXT NOT NULL
) STRICT;
CREATE UNIQUE INDEX "idx_compatclient_clientid" ON compat_clients (client_id);
CREATE TABLE `meta`
(
meta_key TEXT NOT NULL,
value_int INTEGER NULL,
value_txt TEXT NULL,
value_real REAL NULL,
value_blob BLOB NULL,
PRIMARY KEY (meta_key)
) STRICT;
INSERT INTO meta (meta_key, value_int) VALUES ('schema', 3)

View File

@@ -0,0 +1,20 @@
DROP INDEX idx_messages_owner_channel;
DROP INDEX idx_messages_owner_channel_nc;
DROP INDEX idx_messages_idempotency;
CREATE UNIQUE INDEX "idx_messages_idempotency" ON messages (sender_user_id, usr_message_id COLLATE BINARY);
DROP INDEX idx_messages_usedkey;
CREATE INDEX "idx_messages_usedkey" ON messages (sender_user_id, used_key_id);
ALTER TABLE messages DROP COLUMN owner_user_id;

View File

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

View File

@@ -4,19 +4,20 @@ go 1.19
require (
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.14.1
github.com/go-playground/validator/v10 v10.15.5
github.com/go-sql-driver/mysql v1.7.1
github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-sqlite3 v1.14.17
github.com/rs/zerolog v1.29.1
gogs.mikescher.com/BlackForestBytes/goext v0.0.163
github.com/rs/zerolog v1.31.0
gogs.mikescher.com/BlackForestBytes/goext v0.0.291
gopkg.in/loremipsum.v1 v1.1.2
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -26,18 +27,20 @@ require (
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
go.mongodb.org/mongo-driver v1.12.1 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,15 +1,19 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
@@ -19,8 +23,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
@@ -28,6 +32,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -36,20 +42,21 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
@@ -58,14 +65,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -75,36 +84,70 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
gogs.mikescher.com/BlackForestBytes/goext v0.0.163 h1:GYC34wLOdBM/CgAov0AyznfHGd09Km106Ijmp8cZmp4=
gogs.mikescher.com/BlackForestBytes/goext v0.0.163/go.mod h1:Tood+vqmPqS/meYRnUcGz837wqHkP8BykVpY1h8TWoI=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
gogs.mikescher.com/BlackForestBytes/goext v0.0.291 h1:yiRHw8wV9LEZUKljvQ2MBHgeSAI6ZzajDS+FJRGnzVc=
gogs.mikescher.com/BlackForestBytes/goext v0.0.291/go.mod h1:TtMY+/pv09fOldLO3noZ6Yfcy9KU9MjVQ0KQ9nraTeI=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/loremipsum.v1 v1.1.2 h1:12APklfJKuGszqZsrArW5QoQh03/W+qyCCjvnDuS6Tw=
@@ -112,4 +155,5 @@ gopkg.in/loremipsum.v1 v1.1.2/go.mod h1:TuRvzFuzuejXj+odBU6Tubp/EPUyGb9wmSvHenyP
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -1,6 +1,7 @@
package jobs
import (
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"errors"
@@ -132,7 +133,7 @@ func (j *DeliveryRetryJob) execute() (fastrr bool, err error) {
return len(deliveries) == 32, nil
}
func (j *DeliveryRetryJob) redeliver(ctx *logic.SimpleContext, delivery models.Delivery) {
func (j *DeliveryRetryJob) redeliver(ctx *simplectx.SimpleContext, delivery models.Delivery) {
client, err := j.app.Database.Primary.GetClient(ctx, delivery.ReceiverUserID, delivery.ReceiverClientID)
if err != nil {
@@ -168,7 +169,7 @@ func (j *DeliveryRetryJob) redeliver(ctx *logic.SimpleContext, delivery models.D
var msgidOverride *string = nil
if isCompatClient {
messageIdComp, err := j.app.Database.Primary.ConvertToCompatIDOrCreate(ctx, msg.MessageID.String(), "messageid")
messageIdComp, err := j.app.Database.Primary.ConvertToCompatIDOrCreate(ctx, "messageid", msg.MessageID.String())
if err != nil {
log.Err(err).Str("MessageID", delivery.MessageID.String()).Str("ClientID", client.ClientID.String()).Msg("Failed to query/create messageid")
ctx.RollbackTransaction()

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