Compare commits

...

58 Commits

Author SHA1 Message Date
Mikescher e98a804efc Fix panic in /preview/channel/{id}
Build Docker and Deploy / Build Docker Container (push) Successful in 1m49s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 7m56s
Build Docker and Deploy / Deploy to Server (push) Successful in 39s
2026-03-27 12:57:19 +01:00
Mikescher 1f9abb8574 WebApp: Fix channel-detail page for non-owned channels
Build Docker and Deploy / Build Docker Container (push) Successful in 1m48s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 4m11s
Build Docker and Deploy / Deploy to Server (push) Successful in 22s
2026-03-26 17:05:51 +01:00
Mikescher 9352ff5c2c UI improvements
Build Docker and Deploy / Build Docker Container (push) Successful in 51s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 7m29s
Build Docker and Deploy / Deploy to Server (push) Successful in 7s
2026-01-19 19:19:43 +01:00
Mikescher 1dafab8f5c skip some tests [skip-tests]
Build Docker and Deploy / Run Unit-Tests (push) Has been skipped
Build Docker and Deploy / Build Docker Container (push) Successful in 50s
Build Docker and Deploy / Deploy to Server (push) Successful in 24s
2026-01-19 19:07:35 +01:00
Mikescher b5e098a694 Show more data in webapp deliveries-table
Build Docker and Deploy / Build Docker Container (push) Successful in 1m25s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 10m51s
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2026-01-19 18:49:38 +01:00
Mikescher 08fd34632a Fix tests 2026-01-19 18:30:34 +01:00
Mikescher a7a2474e2a Increase quota
Build Docker and Deploy / Build Docker Container (push) Successful in 1m3s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 8m51s
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2025-12-18 15:41:17 +01:00
Mikescher e15d70dd0e [Flutter] Force a username before subscribing 2025-12-18 15:30:58 +01:00
Mikescher e98882a0c6 Skip [TestRequestLogAPI] test
Build Docker and Deploy / Build Docker Container (push) Successful in 54s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m2s
Build Docker and Deploy / Deploy to Server (push) Successful in 18s
2025-12-18 15:25:15 +01:00
Mikescher 24bf7cd434 Fix notification sounds under iOS
Build Docker and Deploy / Build Docker Container (push) Successful in 1m38s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 9m23s
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2025-12-18 15:02:51 +01:00
Mikescher 54c4f873fc [Flutter] Use deviceName instead of hostName for clients 2025-12-18 14:57:46 +01:00
Mikescher b2de793758 [Flutter] Fix sending notifications without content 2025-12-18 14:36:45 +01:00
Mikescher 55a91956ce Implement /shoutrrr endpoint
Build Docker and Deploy / Build Docker Container (push) Successful in 1m20s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m51s
Build Docker and Deploy / Deploy to Server (push) Successful in 27s
2025-12-18 11:36:15 +01:00
Mikescher 202603d16c More webapp changes+fixes
Build Docker and Deploy / Build Docker Container (push) Successful in 1m45s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m31s
Build Docker and Deploy / Deploy to Server (push) Successful in 22s
2025-12-09 16:45:51 +01:00
Mikescher c81143ecdc More webapp changes+fixes
Build Docker and Deploy / Build Docker Container (push) Successful in 1m0s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m18s
Build Docker and Deploy / Deploy to Server (push) Successful in 22s
2025-12-07 04:21:11 +01:00
Mikescher 2b7950f5dc More webapp changes+fixes
Build Docker and Deploy / Build Docker Container (push) Successful in 1m41s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m31s
Build Docker and Deploy / Deploy to Server (push) Successful in 18s
2025-12-05 21:39:32 +01:00
Mikescher c554479604 Implement /deliveries route 2025-12-05 16:59:56 +01:00
Mikescher 8e7a540c97 More webapp changes+fixes 2025-12-05 16:58:30 +01:00
Mikescher c66cd0568f Merge branch 'test/remove_userid_param' 2025-12-05 14:30:55 +01:00
Mikescher 0800d25b30 Remove required user_id param when sending messages 2025-12-05 14:30:44 +01:00
Mikescher 6d180aea38 Remove delete-channel from webapp 2025-12-04 09:16:47 +01:00
Mikescher 3c45191d11 fix broken links on non-owned channels
Build Docker and Deploy / Build Docker Container (push) Successful in 1m7s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 10m56s
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2025-12-03 22:38:24 +01:00
Mikescher 9db56f6db6 Revert "Add sound to iOS notification"
Build Docker and Deploy / Build Docker Container (push) Successful in 1m9s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m5s
Build Docker and Deploy / Deploy to Server (push) Successful in 7s
This reverts commit 26d2854617.
2025-12-03 22:17:43 +01:00
Mikescher 5e6060e537 fix cicd
Build Docker and Deploy / Build Docker Container (push) Successful in 1m4s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m10s
Build Docker and Deploy / Deploy to Server (push) Successful in 8s
2025-12-03 21:36:50 +01:00
Mikescher 4b8ebf15d2 add support for page-based cursortokens (like goext)
Build Docker and Deploy / Build Docker Container (push) Successful in 52s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 2m49s
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2025-12-03 19:46:09 +01:00
Mikescher d26f18f356 remove generated files from git 2025-12-03 19:42:05 +01:00
Mikescher 6090319b5f Simple Managment webapp [LLM] 2025-12-03 19:38:15 +01:00
Mikescher 3ed323e056 update goext to 614 2025-12-03 19:09:51 +01:00
Mikescher f41ef30121 Merge branch 'webapp'
Build Docker and Deploy / Build Docker Container (push) Successful in 49s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 8m43s
Build Docker and Deploy / Deploy to Server (push) Successful in 7s
2025-12-03 19:07:30 +01:00
Mikescher 01df2b49f6 Simple Managment webapp [LLM] 2025-12-03 19:07:23 +01:00
Mikescher 8306992533 Simple Managment webapp [LLM] 2025-12-03 19:03:19 +01:00
Mikescher d983737239 swagger fixes 2025-12-03 18:38:16 +01:00
Mikescher 7c88281f03 Simple Managment webapp [LLM] 2025-12-03 18:38:10 +01:00
Mikescher 308d6bbba0 Simple Managment webapp [LLM] 2025-12-03 18:35:19 +01:00
Mikescher 85e6e4adfb swagger fixes 2025-12-03 18:01:40 +01:00
Mikescher c860ef9c30 Simple Managment webapp [LLM] 2025-12-03 18:00:42 +01:00
Mikescher e7f613b5dc Simple Managment webapp [LLM] 2025-12-03 17:24:57 +01:00
Andi d932410802 Add missing comma
Build Docker and Deploy / Build Docker Container (push) Successful in 56s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 8m43s
Build Docker and Deploy / Deploy to Server (push) Successful in 29s
2025-12-03 16:43:05 +01:00
Andi 26d2854617 Add sound to iOS notification
Build Docker and Deploy / Build Docker Container (push) Failing after 42s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 2m24s
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2025-12-03 16:37:21 +01:00
Mikescher b521f74951 Rename "KeyToken" to "Used Key"
Build Docker and Deploy / Build Docker Container (push) Successful in 1m4s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 8m44s
Build Docker and Deploy / Deploy to Server (push) Successful in 7s
2025-11-11 21:55:04 +01:00
Mikescher acc23c0d10 Update release-script
Build Docker and Deploy / Build Docker Container (push) Successful in 1m25s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m29s
Build Docker and Deploy / Deploy to Server (push) Successful in 41s
2025-11-11 16:05:05 +01:00
Mikescher 693d2ad79e Improve release script 2025-11-11 14:51:06 +01:00
Mikescher f19e8950e8 Fix confusion in AppSettings::load() 2025-11-11 14:38:25 +01:00
Mikescher 64d0541dc6 Add appstore links [skip-tests]
Build Docker and Deploy / Run Unit-Tests (push) Has been skipped
Build Docker and Deploy / Build Docker Container (push) Successful in 43s
Build Docker and Deploy / Deploy to Server (push) Successful in 8s
2025-11-10 15:20:17 +01:00
Mikescher dfb4d9d9e5 add privacy-policy [skip-tests]
Build Docker and Deploy / Run Unit-Tests (push) Has been skipped
Build Docker and Deploy / Build Docker Container (push) Successful in 1m9s
Build Docker and Deploy / Deploy to Server (push) Successful in 8s
2025-11-10 15:13:28 +01:00
Andi 6306555a30 Integrate CocoaPods support and update project configurations for iOS and macOS builds.
Build Docker and Deploy / Build Docker Container (push) Successful in 1m8s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m16s
Build Docker and Deploy / Deploy to Server (push) Successful in 8s
2025-11-10 14:11:30 +01:00
Andi b6b1743285 Clamp GitStamp substring length to avoid out-of-range errors. 2025-11-10 14:07:47 +01:00
Mikescher f6a48140b4 Fix buildscript
Build Docker and Deploy / Build Docker Container (push) Successful in 1m22s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m13s
Build Docker and Deploy / Deploy to Server (push) Successful in 47s
2025-11-09 23:53:36 +01:00
Mikescher cd79cf4449 Upgrade flutter 2025-11-09 23:07:26 +01:00
Mikescher 1aadd9c368 Add filter to subscription-list 2025-11-09 22:51:37 +01:00
Mikescher febc0a8f43 Hide a bunch of expert-properties by default 2025-11-09 22:00:29 +01:00
Mikescher fd5e714074 Add various informative alert-boxes 2025-11-09 21:31:42 +01:00
Mikescher c108859899 Show ShowTokenModal after account creation 2025-11-09 20:34:07 +01:00
Mikescher b3083d37c3 Add button under account to show userid+tokens 2025-11-09 20:27:59 +01:00
Mikescher 521c1e94c0 Allow accessing app-settings even if not logged-in [skip-ci]
Build Docker and Deploy / Build Docker Container (push) Has been skipped
Build Docker and Deploy / Run Unit-Tests (push) Has been skipped
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2025-11-06 11:12:20 +01:00
Mikescher 31a45bc4c3 Fix swagger errors
Build Docker and Deploy / Build Docker Container (push) Successful in 1m27s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 8m57s
Build Docker and Deploy / Deploy to Server (push) Failing after 8s
2025-11-02 23:13:31 +01:00
Mikescher b6944d1dbb Add images/etc for playstore
Build Docker and Deploy / Build Docker Container (push) Successful in 1m15s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 11m30s
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2025-06-11 16:55:48 +02:00
Mikescher 32ef2c5023 skip purchase init on non-mobile platforms 2025-06-11 15:53:10 +02:00
221 changed files with 26763 additions and 9296 deletions
@@ -10,7 +10,7 @@
#
name: Build Docker and Deploy
run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }}
run-name: "[cicd-server]: ${{ github.event.head_commit.message }}"
on:
push:
@@ -30,6 +30,7 @@ jobs:
- name: Check out code
uses: actions/checkout@v3
- run: cd "${{ gitea.workspace }}/scnserver" && make clean
- run: cd "${{ gitea.workspace }}/scnserver" && make dgi
- run: cd "${{ gitea.workspace }}/scnserver" && make docker
- run: cd "${{ gitea.workspace }}/scnserver" && make push-docker
@@ -60,21 +61,7 @@ jobs:
run: go version
- name: Run tests
run: cd "${{ gitea.workspace }}/scnserver" && make dgi && make swagger && SCN_TEST_LOGLEVEL=WARN make test
- name: Send failure mail
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.fastmail.com
server_port: 465
secure: true
username: ${{secrets.MAIL_USERNAME}}
password: ${{secrets.MAIL_PASSWORD}}
subject: Pipeline on '${{ gitea.repository }}' failed
to: ${{ steps.commiter_info.outputs.MAIL }}
from: Gitea Actions <gitea_actions@blackforestbytes.de>
body: "Go to https://gogs.blackforestbytes.com/${{ gitea.repository }}/actions"
run: cd "${{ gitea.workspace }}/scnserver" && make generate && SCN_TEST_LOGLEVEL=WARN make test
deploy_server:
name: Deploy to Server
+57
View File
@@ -0,0 +1,57 @@
# 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
# Configurable with a few commit messages:
# - [skip-tests] Skip the test stage
# - [skip-deployment] Skip the deployment stage
# - [skip-ci] Skip all stages (the whole ci/cd)
#
name: Build Docker and Deploy
run-name: "[cicd-webapp]: ${{ github.event.head_commit.message }}"
on:
push:
branches: ['master']
jobs:
build_webapp:
name: Build Docker Container
runs-on: bfb-cicd-latest
if: >-
!contains(github.event.head_commit.message, '[skip-ci]') &&
!contains(github.event.head_commit.message, '[skip-deployment]')
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 }}/webapp" && make clean
- run: cd "${{ gitea.workspace }}/webapp" && make docker
- run: cd "${{ gitea.workspace }}/webapp" && make push-docker
deploy_webapp:
name: Deploy to Server
needs: [build_webapp]
runs-on: ubuntu-latest
if: >-
!cancelled() &&
!contains(github.event.head_commit.message, '[skip-ci]') &&
!contains(github.event.head_commit.message, '[skip-deployment]') &&
needs.build_webapp.result == 'success'
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-webapp.sh master "${{ gitea.sha }}" || exit 1
Binary file not shown.
+2
View File
@@ -20,9 +20,11 @@ _releases/*
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
+15 -15
View File
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "41456452f29d64e8deb623a3c927524bcf9f111b"
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: app
@@ -13,26 +13,26 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: android
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: ios
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: linux
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: macos
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: web
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: windows
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
# User provided section
+14 -26
View File
@@ -1,7 +1,7 @@
HASH=$(shell git rev-parse HEAD)
VERS=$(shell cat pubspec.yaml | grep -oP '(?<=version: ).*' | sed 's/[\s]*//' | tr -d '\n') # lazy evaluated!
VERS=$(shell cat pubspec.yaml | grep -oP '(?<=version: ).*' | sed 's/[\s]*//' | tr -d '\n' | tr -d '') # lazy evaluated!
java:
sudo archlinux-java set java-17-openjdk
@@ -21,7 +21,7 @@ run-web: java gen
run-android: java gen
ping -c1 10.10.10.177
adb connect 10.10.10.177:5555
flutter pub run build_runner build
dart run build_runner build
_JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555
install-release: java gen
@@ -30,25 +30,7 @@ install-release: java gen
flutter run --release -d 35221JEHN07157
release: java gen
flutter build apk --release
cp build/app/outputs/flutter-apk/app-release.apk "_releases/v$(VERS).apk"
@echo ""
@echo "--> copied APK to _releases ( Version: $(VERS) )"
@echo ""
flutter build appbundle --release
cp build/app/outputs/bundle/release/app-release.aab "_releases/v$(VERS).aab"
cd "build/app/intermediates/merged_native_libs/release/out/lib" && zip -r "../../../../../../../_releases/v$(VERS).symbols.zip" .
@echo ""
@echo "--> copied AAB to _releases ( Version: $(VERS) )"
@echo ""
flutter build linux --release
tar -czf "_releases/v$(VERS).tar.gz" -C build/linux/x64/release/bundle .
@echo ""
@echo "--> copied linux-binary to _releases ( Version: $(VERS) )"
@echo ""
@echo "#=> file://$(shell pwd)/_releases"
@echo ""
@echo "Done."
@_utils/release.sh
test:
dart analyze
@@ -63,24 +45,30 @@ gen: java
# run `make run` in another terminal (or another variant of flutter run)
autoreload:
@
@_utils/autoreload.sh
icons:
flutter pub run flutter_launcher_icons -f "flutter_launcher_icons.yaml"
clean:
cd android && ./gradlew clean
flutter clean
flutter pub get
clean-ios: clean
cd ios && xcodebuild clean && rm -rf Pods Podfile.lock && pod install
clean-android: clean
cd android && ./gradlew clean
# upgrade all packages (add --major-versions even updates across new major versions)
# https://docs.flutter.dev/release/upgrade
# upgrading flutter can be done via `flutter upgrade`: https://docs.flutter.dev/release/upgrade
# android/gradle updates should be done via androidStudio: https://docs.flutter.dev/release/breaking-changes/android-java-gradle-migration-guide
upgrade:
upgrade:
flutter upgrade
flutter pub upgrade
flutter doctor
aider:
aider --model gemini-2.5-pro --no-auto-commits --no-dirty-commits --test-cmd "flutter build linux" --auto-test --subtree-only
aider --model gemini-2.5-pro --no-auto-commits --no-dirty-commits --test-cmd "flutter build linux" --auto-test --subtree-only
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
if [[ -d ".git" ]]; then
echo "Must be called in project root"
exit 1
fi
VERS="$(cat pubspec.yaml | grep -oP '(?<=version: ).*' | sed 's/[\s]*//' | tr -d '\n' | tr -d '')"
VERS_BY_SPEC="$( echo -n "$VERS" | awk -F'+' '{print "v"$1}' )"
VERS_BY_TAG="$(git describe --abbrev=0 --tags)"
if [[ "$VERS_BY_TAG" != "$VERS_BY_SPEC" ]]; then
echo "Version in pubspec.yaml ($VERS_BY_SPEC) does not match latest git tag ($VERS_BY_TAG)"
exit 1
fi
echo ""
echo "(!) Make sure you've updated version-number in pubspec.yaml (current = ${VERS}) and created a tag (current = ${VERS_BY_TAG}) !"
echo '> Press Enter to confirm...' && read -r
echo ""
flutter build apk --release
cp build/app/outputs/flutter-apk/app-release.apk "_releases/v${VERS}.apk"
echo ""
echo "--> copied APK to _releases ( Version: ${VERS} )"
echo ""
flutter build appbundle --release
cp build/app/outputs/bundle/release/app-release.aab "_releases/v${VERS}.aab"
pushd "build/app/intermediates/merged_native_libs/release/out/lib" || exit 1
zip -r "../../../../../../../_releases/v${VERS}.symbols.zip" .
popd || exit 1
echo ""
echo "--> copied AAB to _releases ( Version: ${VERS} )"
echo ""
flutter build linux --release
tar -czf "_releases/v${VERS}.tar.gz" -C build/linux/x64/release/bundle .
echo ""
echo "--> copied linux-binary to _releases ( Version: ${VERS} )"
echo ""
echo "#=> file://$(pwd)/_releases"
echo ""
echo "Done."
+4
View File
@@ -3,6 +3,10 @@ include:
- package:lints/recommended.yaml
- package:flutter_lints/flutter.yaml
formatter:
page_width: 512
trailing_commas: preserve
linter:
+1 -1
View File
@@ -34,7 +34,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace "com.blackforestbytes.simplecloudnotifier"
compileSdkVersion flutter.compileSdkVersion
ndkVersion "27.0.12077973" // should be `flutter.ndkVersion` - but some plugins need 27, even though flutter still has the default value of 26 (flutter 3.29 | 2025-04-12)
ndkVersion flutter.ndkVersion
compileOptions {
coreLibraryDesugaringEnabled true
+1 -1
View File
@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>13.0</string>
</dict>
</plist>
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+184 -28
View File
@@ -8,12 +8,14 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
483C144A8FD9FBE0BE4CA31D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 20818F4FAA848E7B0E1981A1 /* Pods_RunnerTests.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
F0CF5BB613220E8F87B9D978 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDC5CDC3E1AFF7C5BC49C07E /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -40,12 +42,17 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
12F3806130EB6FDA0BB8224F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
20818F4FAA848E7B0E1981A1 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7CD878882EBBB898002F3C7D /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -53,8 +60,12 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
A4D58F2C21BD1C00F0A1FE7D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
AE13A81982696D2690FB1CAB /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
D208168B6EAB5683BAD27E86 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
DF96ECE6A0AC0BFA8724174B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
EDC5CDC3E1AFF7C5BC49C07E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EE5AE3E3797C8A45B5AFA537 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -62,12 +73,51 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F0CF5BB613220E8F87B9D978 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
985FE5E285D4547AB0054913 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
483C144A8FD9FBE0BE4CA31D /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
04ACC4FC139085193359BE10 /* Pods */ = {
isa = PBXGroup;
children = (
A4D58F2C21BD1C00F0A1FE7D /* Pods-Runner.debug.xcconfig */,
EE5AE3E3797C8A45B5AFA537 /* Pods-Runner.release.xcconfig */,
12F3806130EB6FDA0BB8224F /* Pods-Runner.profile.xcconfig */,
DF96ECE6A0AC0BFA8724174B /* Pods-RunnerTests.debug.xcconfig */,
AE13A81982696D2690FB1CAB /* Pods-RunnerTests.release.xcconfig */,
D208168B6EAB5683BAD27E86 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
51BA414B0BC240BA4DED076E /* Frameworks */ = {
isa = PBXGroup;
children = (
EDC5CDC3E1AFF7C5BC49C07E /* Pods_Runner.framework */,
20818F4FAA848E7B0E1981A1 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -79,14 +129,6 @@
name = Flutter;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
@@ -94,6 +136,8 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
04ACC4FC139085193359BE10 /* Pods */,
51BA414B0BC240BA4DED076E /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -109,6 +153,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
7CD878882EBBB898002F3C7D /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -128,9 +173,10 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
827CC5F079B73B9074C059BC /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807E294A63A400263BE5 /* Frameworks */,
331C807F294A63A400263BE5 /* Resources */,
985FE5E285D4547AB0054913 /* Frameworks */,
);
buildRules = (
);
@@ -146,12 +192,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
1A8E37578D7C990159141841 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
81B31B4B9605351C20E486B9 /* [CP] Embed Pods Frameworks */,
14FC8825887E7053BC1CD5F8 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -169,7 +218,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
@@ -223,6 +272,45 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
14FC8825887E7053BC1CD5F8 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
1A8E37578D7C990159141841 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -239,6 +327,45 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
81B31B4B9605351C20E486B9 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
827CC5F079B73B9074C059BC /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -308,6 +435,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -337,6 +465,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -345,7 +474,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -356,19 +485,26 @@
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
baseConfigurationReference = 12F3806130EB6FDA0BB8224F /* Pods-Runner.profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = H5FLAAV8F2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SimpleCloudNotifier;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.simplecloudnotifier;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.SimpleCloudNotifier;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -377,7 +513,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = DF96ECE6A0AC0BFA8724174B /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -395,7 +531,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = AE13A81982696D2690FB1CAB /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -411,7 +547,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = D208168B6EAB5683BAD27E86 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -427,8 +563,10 @@
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -458,6 +596,7 @@
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -472,7 +611,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -482,8 +621,10 @@
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -513,6 +654,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -521,7 +663,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -534,19 +676,26 @@
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
baseConfigurationReference = A4D58F2C21BD1C00F0A1FE7D /* Pods-Runner.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = H5FLAAV8F2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SimpleCloudNotifier;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.simplecloudnotifier;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.SimpleCloudNotifier;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -556,19 +705,26 @@
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
baseConfigurationReference = EE5AE3E3797C8A45B5AFA537 /* Pods-Runner.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = H5FLAAV8F2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SimpleCloudNotifier;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.simplecloudnotifier;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.SimpleCloudNotifier;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@@ -54,11 +55,13 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
+3
View File
@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+1 -1
View File
@@ -2,7 +2,7 @@ import UIKit
import Flutter
import flutter_local_notifications
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
+13 -9
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -24,6 +26,17 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photos access to get QR code from photo library</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -41,14 +54,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photos access to get QR code from photo library</string>
</dict>
</plist>
@@ -2,7 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
+22 -8
View File
@@ -54,6 +54,20 @@ class MessageFilter {
});
}
class SubscriptionFilter {
static final SubscriptionFilter ALL = SubscriptionFilter('both', 'all', 'all');
static final SubscriptionFilter OWNED_INACTIVE = SubscriptionFilter('outgoing', 'unconfirmed', 'false');
static final SubscriptionFilter OWNED_ACTIVE = SubscriptionFilter('outgoing', 'confirmed', 'false');
static final SubscriptionFilter EXTERNAL_ALL = SubscriptionFilter('outgoing', 'all', 'true');
static final SubscriptionFilter INCOMING_ALL = SubscriptionFilter('incoming', 'all', 'true');
final String direction; // 'outgoing' | 'incoming' | 'both'
final String confirmation; // 'confirmed' | 'unconfirmed' | 'all'
final String external; // 'true' | 'false' | 'all'
SubscriptionFilter(this.direction, this.confirmation, this.external) {}
}
class APIClient {
static const String _base = 'https://simplecloudnotifier.de';
static const String _prefix = '/api/v2';
@@ -123,7 +137,7 @@ class APIClient {
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
Toaster.error("Error", apierr.message);
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message);
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message, true);
}
try {
@@ -191,7 +205,7 @@ class APIClient {
fn: User.fromJson,
authToken: auth.getToken(),
query: {
'confirm': ['true']
'confirm': ['true'],
},
);
}
@@ -216,7 +230,7 @@ class APIClient {
static Future<Client> updateClient(TokenSource auth, String clientID, {String? fcmToken, String? agentModel, String? name, String? agentVersion}) async {
return await _request(
name: 'updateClient',
method: 'PUT',
method: 'PATCH',
relURL: 'users/${auth.getUserID()}/clients/$clientID',
jsonBody: {
if (fcmToken != null) 'fcm_token': fcmToken,
@@ -245,7 +259,7 @@ class APIClient {
method: 'GET',
relURL: 'users/${auth.getUserID()}/channels',
query: {
'selector': [sel.apiKey]
'selector': [sel.apiKey],
},
fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels'] as List<dynamic>),
authToken: auth.getToken(),
@@ -345,15 +359,15 @@ class APIClient {
);
}
static Future<List<Subscription>> getSubscriptionList(TokenSource auth) async {
static Future<List<Subscription>> getSubscriptionList(TokenSource auth, SubscriptionFilter filter) async {
return await _request(
name: 'getSubscriptionList',
method: 'GET',
relURL: 'users/${auth.getUserID()}/subscriptions',
query: {
'direction': ['both'],
'confirmation': ['all'],
'external': ['all'],
'direction': [filter.direction],
'confirmation': [filter.confirmation],
'external': [filter.external],
},
fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List<dynamic>),
authToken: auth.getToken(),
+2 -1
View File
@@ -3,8 +3,9 @@ class APIException implements Exception {
final int error;
final int errHighlight;
final String message;
final bool toastShown;
APIException(this.httpStatus, this.error, this.errHighlight, this.message);
APIException(this.httpStatus, this.error, this.errHighlight, this.message, this.toastShown);
@override
String toString() {
@@ -2,50 +2,104 @@ import 'package:flutter/material.dart';
enum BadgeMode { error, warn, info }
class BadgeDisplay extends StatelessWidget {
class BadgeDisplay extends StatefulWidget {
final String text;
final BadgeMode mode;
final IconData? icon;
final TextAlign textAlign;
final bool closable;
final EdgeInsets extraPadding;
final bool hidden;
const BadgeDisplay({
Key? key,
required this.text,
required this.mode,
required this.icon,
this.textAlign = TextAlign.center,
this.closable = false,
this.extraPadding = EdgeInsets.zero,
this.hidden = false,
}) : super(key: key);
@override
State<BadgeDisplay> createState() => _BadgeDisplayState();
}
class _BadgeDisplayState extends State<BadgeDisplay> {
bool _isVisible = true;
@override
Widget build(BuildContext context) {
if (!_isVisible || widget.hidden) return const SizedBox.shrink();
var col = Colors.grey;
var colFG = Colors.black;
if (mode == BadgeMode.error) col = Colors.red;
if (mode == BadgeMode.warn) col = Colors.orange;
if (mode == BadgeMode.info) col = Colors.blue;
if (widget.mode == BadgeMode.error) col = Colors.red;
if (widget.mode == BadgeMode.warn) col = Colors.orange;
if (widget.mode == BadgeMode.info) col = Colors.blue;
if (mode == BadgeMode.error) colFG = Colors.red[900]!;
if (mode == BadgeMode.warn) colFG = Colors.black;
if (mode == BadgeMode.info) colFG = Colors.black;
if (widget.mode == BadgeMode.error) colFG = Colors.red[900]!;
if (widget.mode == BadgeMode.warn) colFG = Colors.black;
if (widget.mode == BadgeMode.info) colFG = Colors.black;
return Container(
padding: const EdgeInsets.fromLTRB(8, 2, 8, 2),
decoration: BoxDecoration(
color: col[100],
border: Border.all(color: col[300]!),
borderRadius: BorderRadius.circular(4.0),
),
child: Row(
margin: widget.extraPadding,
child: Stack(
clipBehavior: Clip.none,
children: [
if (icon != null) Icon(icon!, color: colFG, size: 16.0),
Expanded(
child: Text(
text,
textAlign: TextAlign.center,
style: TextStyle(color: colFG, fontSize: 14.0),
// The badge itself
Container(
padding: EdgeInsets.fromLTRB(8, 2, widget.closable ? 16 : 8, 2),
decoration: BoxDecoration(
color: col[100],
border: Border.all(color: col[300]!),
borderRadius: BorderRadius.circular(4.0),
),
child: Row(
children: [
if (widget.icon != null) Icon(widget.icon!, color: colFG, size: 16.0),
Expanded(
child: Text(
widget.text,
textAlign: widget.textAlign,
style: TextStyle(color: colFG, fontSize: 14.0),
),
),
],
),
),
if (widget.closable) _buildCloseButton(context, colFG),
],
),
);
}
Positioned _buildCloseButton(BuildContext context, Color colFG) {
return Positioned(
top: 4,
right: 4,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
setState(() {
_isVisible = false;
});
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(2),
child: Icon(
Icons.close,
size: 14,
color: colFG,
),
),
),
),
);
}
}
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
class FilterChips<T extends Enum> extends StatelessWidget {
final List<(T, String)> options;
final T value;
final void Function(T)? onChanged;
const FilterChips({
Key? key,
required this.options,
required this.value,
required this.onChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var opt in options) _buildChiplet(context, opt.$1, opt.$2),
],
),
);
}
Widget _buildChiplet(BuildContext context, T optValue, String optText) {
final isSelected = optValue == value;
return Padding(
padding: const EdgeInsets.fromLTRB(0, 2, 4, 2),
child: InputChip(
label: Text(
optText,
style: isSelected ? TextStyle(color: Theme.of(context).colorScheme.onPrimary) : null,
),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
isEnabled: true,
selected: true,
showCheckmark: false,
onPressed: () {
if (!isSelected) onChanged?.call(optValue);
},
selectedColor: isSelected ? Theme.of(context).colorScheme.primary : null,
),
);
}
}
+20 -10
View File
@@ -20,20 +20,30 @@ class SCNNavLayout extends StatefulWidget {
}
class _SCNNavLayoutState extends State<SCNNavLayout> {
int _selectedIndex = 0; // 4 == FAB
static const INDEX_MESSAGES = 0;
static const INDEX_CHANNELS = 1;
static const INDEX_ACCOUNT = 2;
static const INDEX_CONFIG = 3;
static const INDEX_SEND = 4; // FAB
int _selectedIndex = INDEX_MESSAGES;
@override
initState() {
final userAcc = Provider.of<AppAuth>(context, listen: false);
if (!userAcc.isAuth()) _selectedIndex = 2;
if (!userAcc.isAuth()) _selectedIndex = INDEX_ACCOUNT;
super.initState();
}
void _onItemTapped(int index) {
final userAcc = Provider.of<AppAuth>(context, listen: false);
if (!userAcc.isAuth()) {
if (!userAcc.isAuth() && [INDEX_MESSAGES, INDEX_CHANNELS, INDEX_SEND].contains(index)) {
Toaster.info("Not logged in", "Please login or create a new account first");
setState(() {
_selectedIndex = INDEX_ACCOUNT;
});
return;
}
@@ -50,7 +60,7 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
}
setState(() {
_selectedIndex = 4;
_selectedIndex = INDEX_SEND;
});
}
@@ -59,17 +69,17 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
return Scaffold(
appBar: SCNAppBar(
title: null,
showSearch: _selectedIndex == 0,
showSearch: _selectedIndex == INDEX_MESSAGES,
showShare: false,
showThemeSwitch: true,
),
body: IndexedStack(
children: [
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage(isVisiblePage: _selectedIndex == 0)),
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage(isVisiblePage: _selectedIndex == 1)),
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage(isVisiblePage: _selectedIndex == 2)),
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage(isVisiblePage: _selectedIndex == 3)),
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage(isVisiblePage: _selectedIndex == 4)),
ExcludeFocus(excluding: _selectedIndex != INDEX_MESSAGES, child: MessageListPage(isVisiblePage: _selectedIndex == INDEX_MESSAGES)),
ExcludeFocus(excluding: _selectedIndex != INDEX_CHANNELS, child: ChannelRootPage(isVisiblePage: _selectedIndex == INDEX_CHANNELS)),
ExcludeFocus(excluding: _selectedIndex != INDEX_ACCOUNT, child: AccountRootPage(isVisiblePage: _selectedIndex == INDEX_ACCOUNT)),
ExcludeFocus(excluding: _selectedIndex != INDEX_CONFIG, child: SettingsRootPage(isVisiblePage: _selectedIndex == INDEX_CONFIG)),
ExcludeFocus(excluding: _selectedIndex != INDEX_SEND, child: SendRootPage(isVisiblePage: _selectedIndex == INDEX_SEND)),
],
index: _selectedIndex,
),
@@ -32,7 +32,7 @@ class _FilterModalPriorityState extends State<FilterModalPriority> {
return AlertDialog(
title: const Text('Priority'),
content: Container(
width: 0,
width: 9000,
height: 200,
child: ListView.builder(
shrinkWrap: true,
+38 -14
View File
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/main_messaging.dart';
import 'package:simplecloudnotifier/main_utils.dart';
import 'package:simplecloudnotifier/components/layout/nav_layout.dart';
@@ -68,15 +69,17 @@ void main() async {
print('[INIT] Request Notification permissions...');
await FirebaseMessaging.instance.requestPermission(provisional: true);
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) {
try {
setFirebaseToken(fcmToken);
} catch (exc, trace) {
ApplicationLog.error('Failed to set firebase token: ' + exc.toString(), trace: trace);
}
}).onError((dynamic err) {
ApplicationLog.error('Failed to listen to token refresh events: ' + (err?.toString() ?? ''));
});
FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) {
try {
setFirebaseToken(fcmToken);
} catch (exc, trace) {
ApplicationLog.error('Failed to set firebase token: ' + exc.toString(), trace: trace);
}
})
.onError((dynamic err) {
ApplicationLog.error('Failed to listen to token refresh events: ' + (err?.toString() ?? ''));
});
try {
print('[INIT] Query firebase token...');
@@ -96,6 +99,25 @@ void main() async {
await appAuth.tryMigrateFromV1();
if (appAuth.isAuth()) {
print('[INIT] Load Client and potentially update...');
try {
var client = await appAuth.loadClient(onlyCached: true);
if (client != null) {
if (client.agentModel != Globals().deviceModel || client.name != Globals().nameForClient() || client.agentVersion != Globals().version) {
print('[INIT] Update Client info...');
final newClient = await APIClient.updateClient(appAuth, client.clientID, agentModel: Globals().deviceModel, name: Globals().nameForClient(), agentVersion: Globals().version);
appAuth.setClientAndClientID(newClient);
await appAuth.save();
}
}
} catch (exc, trace) {
ApplicationLog.error('Failed to get client (on init): ' + exc.toString(), trace: trace);
}
}
print('[INIT] Load Notifications...');
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
@@ -167,11 +189,13 @@ class _SCNAppState extends State<SCNApp> {
@override
void initState() {
_purchaseSubscription = InAppPurchase.instance.purchaseStream.listen(
purchaseUpdated,
onDone: () => _purchaseSubscription?.cancel(),
onError: purchaseError,
);
if (Globals().clientType == 'IOS' || Globals().clientType == 'ANDROID') {
_purchaseSubscription = InAppPurchase.instance.purchaseStream.listen(
purchaseUpdated,
onDone: () => _purchaseSubscription?.cancel(),
onError: purchaseError,
);
}
super.initState();
}
+2 -2
View File
@@ -40,11 +40,11 @@ void setFirebaseToken(String fcmToken) async {
if (client == null) {
// should not really happen - perhaps someone externally deleted the client?
final newClient = await APIClient.addClient(acc, fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType);
final newClient = await APIClient.addClient(acc, fcmToken, Globals().deviceModel, Globals().version, Globals().nameForClient(), Globals().clientType);
acc.setClientAndClientID(newClient);
await acc.save();
} else {
final newClient = await APIClient.updateClient(acc, client.clientID, fcmToken: fcmToken, agentModel: Globals().deviceModel, name: Globals().hostname, agentVersion: Globals().version);
final newClient = await APIClient.updateClient(acc, client.clientID, fcmToken: fcmToken, agentModel: Globals().deviceModel, name: Globals().nameForClient(), agentVersion: Globals().version);
acc.setClientAndClientID(newClient);
await acc.save();
}
+63 -16
View File
@@ -7,9 +7,11 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/account/login.dart';
import 'package:simplecloudnotifier/pages/account/show_token_modal.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_list_extended.dart';
import 'package:simplecloudnotifier/pages/client_list/client_list.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
@@ -115,7 +117,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
_futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
if (!userAcc.isAuth()) throw new Exception('not logged in');
final subs = await APIClient.getSubscriptionList(userAcc);
final subs = await APIClient.getSubscriptionList(userAcc, SubscriptionFilter.ALL);
return subs.length;
}());
@@ -151,7 +153,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
// refresh all data and then replace teh futures used in build()
final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all);
final subs = await APIClient.getSubscriptionList(userAcc);
final subs = await APIClient.getSubscriptionList(userAcc, SubscriptionFilter.ALL);
final clients = await APIClient.getClientList(userAcc);
final keys = await APIClient.getKeyTokenList(userAcc);
final senderNames = await APIClient.getSenderNameList(userAcc);
@@ -165,6 +167,9 @@ class _AccountRootPageState extends State<AccountRootPage> {
_futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
_futureUser = ImmediateFuture.ofValue(user);
});
} on APIException catch (exc, trace) {
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
if (!exc.toastShown) Toaster.error("Error", 'Failed to refresh account data');
} catch (exc, trace) {
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to refresh account data');
@@ -268,7 +273,20 @@ class _AccountRootPageState extends State<AccountRootPage> {
_buildHeader(context, user),
const SizedBox(height: 16),
Text(user.username ?? user.userID, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
const SizedBox(height: 16),
const SizedBox(height: 8),
if (acc.tokenSend != null || acc.tokenAdmin != null)
Row(
children: [
Expanded(
child: UI.button(
text: 'UserID & Token kopieren',
onPressed: () => _showTokenModal(context, acc),
icon: FontAwesomeIcons.copy,
),
),
],
),
const SizedBox(height: 8),
..._buildCards(context, user),
SizedBox(height: 16),
_buildFooter(context, user),
@@ -403,7 +421,13 @@ class _AccountRootPageState extends State<AccountRootPage> {
],
),
onTap: () {
Navi.push(context, () => FilteredMessageViewPage(title: "All Messages", filter: MessageFilter(senderUserID: [user.userID])));
Navi.push(
context,
() => FilteredMessageViewPage(
title: "All Messages",
alertText: "All messages sent from your account",
filter: MessageFilter(senderUserID: [user.userID]),
));
},
),
];
@@ -456,18 +480,20 @@ class _AccountRootPageState extends State<AccountRootPage> {
child: Row(
children: [
Expanded(
child: UI.button(
text: 'Logout',
onPressed: _logout,
color: Colors.orange,
)),
child: UI.button(
text: 'Logout',
onPressed: _logout,
color: Colors.orange,
),
),
const SizedBox(width: 8),
Expanded(
child: UI.button(
text: 'Delete Account',
onPressed: _deleteAccount,
color: Colors.red,
)),
child: UI.button(
text: 'Delete Account',
onPressed: _deleteAccount,
color: Colors.red,
),
),
],
),
);
@@ -501,15 +527,23 @@ class _AccountRootPageState extends State<AccountRootPage> {
await Globals().setPrefFCMToken(fcmToken);
final user = await APIClient.createUserWithClient(null, fcmToken, Globals().platform, Globals().version, Globals().hostname, Globals().clientType);
final user = await APIClient.createUserWithClient(null, fcmToken, Globals().platform, Globals().version, Globals().nameForClient(), Globals().clientType);
acc.set(user.user, user.clients[0], user.adminKey, user.sendKey);
await acc.save();
Toaster.success("Success", 'Successfully Created a new account');
} catch (exc, trace) {
showDialog<void>(
context: context,
builder: (context) => ShowTokenModal(account: acc, isAfterRegister: true),
);
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to create user account');
ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to create user account');
ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace);
} finally {
setState(() => loading = false);
}
@@ -553,6 +587,9 @@ class _AccountRootPageState extends State<AccountRootPage> {
//TODO clear messages/channels/etc in open views
acc.clear();
await acc.save();
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to delete user');
ApplicationLog.error('Failed to delete user: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to delete user');
ApplicationLog.error('Failed to delete user: ' + exc.toString(), trace: trace);
@@ -580,6 +617,9 @@ class _AccountRootPageState extends State<AccountRootPage> {
Toaster.success("Success", 'Username changed');
_backgroundRefresh();
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to update username');
ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to update username');
ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace);
@@ -717,4 +757,11 @@ class _AccountRootPageState extends State<AccountRootPage> {
return completer.future;
}
void _showTokenModal(BuildContext context, AppAuth acc) {
showDialog<void>(
context: context,
builder: (context) => ShowTokenModal(account: acc, isAfterRegister: false),
);
}
}
+6 -2
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
@@ -155,16 +156,19 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
final user = await APIClient.getUser(DirectTokenSource(uid, atokv), uid);
final client = await APIClient.addClient(DirectTokenSource(uid, atokv), fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType);
final client = await APIClient.addClient(DirectTokenSource(uid, atokv), fcmToken, Globals().deviceModel, Globals().version, Globals().nameForClient(), Globals().clientType);
acc.set(user, client, atokv, stokv);
await acc.save();
Toaster.success("Login", "Successfully logged in");
Navi.popToRoot(context);
} catch (exc, trace) {
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to verify token');
ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to verify token');
ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace);
} finally {
setState(() => loading = false);
}
@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class ShowTokenModal extends StatelessWidget {
final AppAuth account;
final bool isAfterRegister;
const ShowTokenModal({Key? key, required this.account, required this.isAfterRegister}) : super(key: key);
@override
Widget build(BuildContext context) {
var alertText = "Use your UserID and Send-Token to send messages to your account.\n\nThe Admin-Token should not be shared.";
if (this.isAfterRegister) {
alertText = "These are your UserID and tokens.\n\nBackup your Admin-Token safely.\n\nUse UserId & Send-Token to send yourself messages.";
}
return AlertDialog(
title: const Text('UserID & Token'),
content: Container(
width: 9000,
height: 450,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
BadgeDisplay(
text: alertText,
icon: null,
mode: BadgeMode.info,
textAlign: TextAlign.left,
),
const SizedBox(height: 16),
if (this.account.userID != null)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'UserID',
values: [this.account.userID!],
iconActions: [(FontAwesomeIcons.copy, null, () => _copy('UserID', this.account.userID!))],
),
const SizedBox(height: 4),
if (this.account.tokenSend != null)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidKey,
title: 'Send-Token',
values: [this.account.tokenSend!],
iconActions: [(FontAwesomeIcons.copy, null, () => _copy('Send-Token', this.account.tokenSend!))],
),
const SizedBox(height: 4),
if (this.account.tokenSend != null)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidKey,
title: 'Admin-Token',
values: [this.account.tokenAdmin!],
iconActions: [(FontAwesomeIcons.copy, null, () => _copy('Admin-Token', this.account.tokenAdmin!))],
),
],
),
),
),
actions: <Widget>[
TextButton(
child: const Text('Close'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
void _copy(String txt, String v) {
Clipboard.setData(new ClipboardData(text: v));
Toaster.info("Clipboard", 'Copied ${txt} to Clipboard');
print('================= [CLIPBOARD] =================\n${v}\n================= [/CLIPBOARD] =================');
}
}
@@ -144,28 +144,7 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
@override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, ChannelWithSubscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
itemBuilder: (context, item, index) => ChannelListItem(
channel: item.channel,
subscription: item.subscription,
mode: ChannelListItemMode.Messages,
onChannelListReloadTrigger: _enqueueReload,
onSubscriptionChanged: (channelID, subscription) {
setState(() {
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
});
},
),
),
),
),
body: _buildList(context),
floatingActionButton: FloatingActionButton(
heroTag: 'fab_channel_list_qr',
onPressed: () {
@@ -176,6 +155,31 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
);
}
Widget _buildList(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, ChannelWithSubscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
itemBuilder: (context, item, index) => ChannelListItem(
channel: item.channel,
subscription: item.subscription,
mode: ChannelListItemMode.Messages,
onChannelListReloadTrigger: _enqueueReload,
onSubscriptionChanged: (channelID, subscription) {
setState(() {
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
});
},
),
),
),
);
}
void _enqueueReload() {
_reloadEnqueued = true;
}
@@ -3,10 +3,12 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
@@ -121,27 +123,21 @@ class _ChannelListExtendedPageState extends State<ChannelListExtendedPage> with
showShare: false,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, ChannelWithSubscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
itemBuilder: (context, item, index) => ChannelListItem(
channel: item.channel,
subscription: item.subscription,
mode: ChannelListItemMode.Extended,
onChannelListReloadTrigger: _enqueueReload,
onSubscriptionChanged: (channelID, subscription) {
setState(() {
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
});
},
),
child: Column(
children: [
BadgeDisplay(
text: "All channels accessible from this account\n(Your own channels and subscribed channels)",
icon: null,
mode: BadgeMode.info,
textAlign: TextAlign.left,
closable: true,
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
hidden: !AppSettings().showInfoAlerts,
),
),
Expanded(
child: _buildList(context),
)
],
),
),
floatingActionButton: FloatingActionButton(
@@ -154,6 +150,31 @@ class _ChannelListExtendedPageState extends State<ChannelListExtendedPage> with
);
}
Widget _buildList(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, ChannelWithSubscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
itemBuilder: (context, item, index) => ChannelListItem(
channel: item.channel,
subscription: item.subscription,
mode: ChannelListItemMode.Extended,
onChannelListReloadTrigger: _enqueueReload,
onSubscriptionChanged: (channelID, subscription) {
setState(() {
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
});
},
),
),
),
);
}
void _enqueueReload() {
_reloadEnqueued = true;
}
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
@@ -220,6 +221,9 @@ class _ChannelListItemState extends State<ChannelListItem> {
} else {
Toaster.success("Success", 'Requested widget.subscription to channel');
}
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
@@ -238,6 +242,9 @@ class _ChannelListItemState extends State<ChannelListItem> {
widget.onSubscriptionChanged.call(sub.channelID, null);
Toaster.success("Success", 'Unsubscribed from channel');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -256,6 +263,9 @@ class _ChannelListItemState extends State<ChannelListItem> {
widget.onSubscriptionChanged.call(sub.channelID, newSub);
Toaster.success("Success", 'Unsubscribed from channel');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -274,6 +284,9 @@ class _ChannelListItemState extends State<ChannelListItem> {
widget.onSubscriptionChanged.call(sub.channelID, newSub);
Toaster.success("Success", 'Subscribed to channel');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
@@ -3,11 +3,13 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/scan_result.dart';
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart';
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_channelview.dart';
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_messagesend.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class ChannelScannerPage extends StatefulWidget {
@@ -40,6 +42,16 @@ class _ChannelScannerPageState extends State<ChannelScannerPage> {
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
children: [
SizedBox(height: 16),
BadgeDisplay(
text: "Scan the QR Code of an account to send messages to this account,\nor the QR Code of an channel to request a subscription to this channel.",
icon: null,
mode: BadgeMode.info,
textAlign: TextAlign.left,
closable: true,
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
hidden: !AppSettings().showInfoAlerts,
),
SizedBox(height: 16),
if (scanResult == null) ...[
Center(
@@ -12,6 +12,7 @@ import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/dialogs.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class ChannelScannerResultChannelSubscribe extends StatefulWidget {
@@ -172,6 +173,38 @@ class _ChannelScannerResultChannelSubscribeState extends State<ChannelScannerRes
void _onSubscribe() async {
final auth = Provider.of<AppAuth>(context, listen: false);
// Check if username is set
try {
final user = await auth.loadUser();
if (user.username == null || user.username!.isEmpty) {
// Show modal to set username
var newusername = await UIDialogs.showUsernameRequiredDialog(context);
if (newusername == null) return; // User cancelled
newusername = newusername.trim();
if (newusername.isEmpty) {
Toaster.error("Error", 'Username cannot be empty');
return;
}
// Update username via API
try {
await APIClient.updateUser(auth, auth.userID!, username: newusername);
await auth.loadUser(force: true); // Refresh cached user
Toaster.success("Success", 'Username set');
} catch (e) {
Toaster.error("Error", 'Failed to set username: ${e.toString()}');
return;
}
}
} catch (e) {
Toaster.error("Error", 'Failed to load user data: ${e.toString()}');
return;
}
// Proceed with subscription
try {
var sub = await APIClient.subscribeToChannelbyID(auth, widget.value.channelID, subscribeKey: widget.value.subscribeKey);
if (sub.confirmed) {
@@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
@@ -14,6 +15,7 @@ import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message
import 'package:simplecloudnotifier/pages/subscription_view/subscription_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/dialogs.dart';
@@ -181,18 +183,20 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
children: [
_buildQRCode(context),
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'ChannelID',
values: [channel.channelID],
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidInputNumeric,
title: 'InternalName',
values: [channel.internalName],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'ChannelID',
values: [channel.channelID],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidInputNumeric,
title: 'InternalName',
values: [channel.internalName],
),
_buildDisplayNameCard(context, true),
_buildDescriptionNameCard(context, true),
UI.metaCard(
@@ -283,18 +287,20 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'ChannelID',
values: [channel.channelID],
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidInputNumeric,
title: 'InternalName',
values: [channel.internalName],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'ChannelID',
values: [channel.channelID],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidInputNumeric,
title: 'InternalName',
values: [channel.internalName],
),
_buildDisplayNameCard(context, false),
_buildDescriptionNameCard(context, false),
subCard,
@@ -305,7 +311,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
icon: FontAwesomeIcons.solidEnvelope,
title: 'Messages',
values: [channel.messagesSent.toString()],
mainAction: (subscription != null && subscription!.confirmed) ? () => Navi.push(context, () => FilteredMessageViewPage(title: channel.displayName, filter: MessageFilter(channelIDs: [channel.channelID]))) : null,
mainAction: (subscription != null && subscription!.confirmed) ? () => Navi.push(context, () => FilteredMessageViewPage(title: channel.displayName, alertText: null, filter: MessageFilter(channelIDs: [channel.channelID]))) : null,
),
],
),
@@ -344,12 +350,20 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
future: _futureOwner.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Owner',
values: [channelPreview!.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
if (AppSettings().showExtendedAttributes)
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Owner',
values: [channelPreview!.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
else
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Owner',
values: [(snapshot.data?.username ?? channelPreview!.ownerUserID) + (isOwned ? ' (you)' : '')],
);
} else {
return UI.metaCard(
context: context,
@@ -537,6 +551,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
});
widget.needsReload?.call();
} on APIException catch (exc, trace) {
ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace);
if (!exc.toastShown) Toaster.error("Error", 'Failed to save DisplayName');
} catch (exc, trace) {
ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to save DisplayName');
@@ -569,9 +586,12 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
});
widget.needsReload?.call();
} on APIException catch (exc, trace) {
ApplicationLog.error('Failed to save DescriptionName: ' + exc.toString(), trace: trace);
if (!exc.toastShown) Toaster.error("Error", 'Failed to save description');
} catch (exc, trace) {
ApplicationLog.error('Failed to save DescriptionName: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to save DescriptionName');
Toaster.error("Error", 'Failed to save description');
}
}
@@ -589,6 +609,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
} else {
Toaster.success("Success", 'Requested subscription to channel');
}
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
@@ -612,6 +635,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false);
Toaster.success("Success", 'Unsubscribed from channel');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -630,6 +656,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false);
Toaster.success("Success", 'Unsubscribed from channel');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -648,6 +677,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false);
Toaster.success("Success", 'Subscribed to channel');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
@@ -664,6 +696,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully revoked');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to revoke subscription');
ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to revoke subscription');
ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace);
@@ -680,6 +715,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully confirmed');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to confirm subscription');
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to confirm subscription');
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
@@ -696,6 +734,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false);
Toaster.success("Success", 'Subscription request succesfully denied');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to deny subscription');
ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to deny subscription');
ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace);
+30 -9
View File
@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/client_list/client_list_item.dart';
@@ -69,16 +71,35 @@ class _ClientListPageState extends State<ClientListPage> {
showShare: false,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, Client>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Client>(
itemBuilder: (context, item, index) => ClientListItem(item: item),
child: Column(
children: [
BadgeDisplay(
text: "All clients connected with this account",
icon: null,
mode: BadgeMode.info,
textAlign: TextAlign.left,
closable: true,
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
hidden: !AppSettings().showInfoAlerts,
),
),
Expanded(
child: _buildList(context),
)
],
),
),
);
}
Widget _buildList(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, Client>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Client>(
itemBuilder: (context, item, index) => ClientListItem(item: item),
),
),
);
@@ -5,6 +5,7 @@ import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/utils/notifier.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
@@ -65,22 +66,31 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
onPressed: _sendTokenToServer,
text: 'Send FCM Token to Server',
),
SizedBox(height: 4),
UI.button(
big: false,
onPressed: _updateClient,
text: 'Update Client on Server',
),
SizedBox(height: 20),
UI.button(
big: false,
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, null),
text: 'Show local notification (generic)',
),
SizedBox(height: 4),
UI.button(
big: false,
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 0),
text: 'Show local notification (Prio = 0)',
),
SizedBox(height: 4),
UI.button(
big: false,
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 1),
text: 'Show local notification (Prio = 1)',
),
SizedBox(height: 4),
UI.button(
big: false,
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 2),
@@ -128,6 +138,26 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
}
}
void _updateClient() async {
try {
final auth = AppAuth();
final clientID = auth.getClientID();
if (clientID == null) {
Toaster.error("Error", "No Client set");
return;
}
final newClient = await APIClient.updateClient(auth, clientID, agentModel: Globals().deviceModel, name: Globals().nameForClient(), agentVersion: Globals().version);
auth.setClientAndClientID(newClient);
Toaster.success("Success", "Client updated");
} catch (exc, trace) {
Toaster.error("Error", "An error occurred while updating the client: ${exc.toString()}");
ApplicationLog.error("An error occurred while updating the client: ${exc.toString()}", trace: trace);
}
}
void _copyToken() async {
try {
final fcmToken = await FirebaseMessaging.instance.getToken();
@@ -135,7 +135,7 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
void _copyCurl() {
final method = '-X ${widget.request.method}';
final header = widget.request.requestHeaders.entries.map((v) => '-H "${v.key}: ${v.value}"').join(' ');
final body = widget.request.requestBody.isNotEmpty ? '-d "${widget.request.requestBody}"' : '';
final body = widget.request.requestBody.isNotEmpty ? '-d \'${widget.request.requestBody}\'' : '';
final curlParts = ['curl', method, header, '"${widget.request.url}"', body];
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
@@ -17,11 +18,13 @@ class FilteredMessageViewPage extends StatefulWidget {
const FilteredMessageViewPage({
required this.title,
required this.filter,
required this.alertText,
super.key,
});
final String title;
final MessageFilter filter;
final String? alertText;
@override
State<FilteredMessageViewPage> createState() => _FilteredMessageViewPageState();
@@ -87,31 +90,52 @@ class _FilteredMessageViewPageState extends State<FilteredMessageViewPage> {
@override
Widget build(BuildContext context) {
Widget child = _buildMessageList(context);
if (widget.alertText != null) {
child = Column(
children: [
BadgeDisplay(
text: widget.alertText!,
icon: null,
mode: BadgeMode.info,
textAlign: TextAlign.left,
closable: true,
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
hidden: !AppSettings().showInfoAlerts,
),
Expanded(
child: child,
)
],
);
}
return SCNScaffold(
title: this.widget.title,
showSearch: false,
showShare: false,
child: _buildMessageList(context),
child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: child,
),
);
}
Widget _buildMessageList(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<String, SCNMessage>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<SCNMessage>(
itemBuilder: (context, item, index) => MessageListItem(
message: item,
allChannels: _channels ?? {},
onPressed: () {
Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,)));
},
),
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<String, SCNMessage>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<SCNMessage>(
itemBuilder: (context, item, index) => MessageListItem(
message: item,
allChannels: _channels ?? {},
onPressed: () {
Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,)));
},
),
),
),
@@ -52,7 +52,7 @@ class _KeyTokenCreateDialogState extends State<KeyTokenCreateDialog> {
return AlertDialog(
title: const Text('Create new key'),
content: Container(
width: 0,
width: 9000,
height: 400,
child: SingleChildScrollView(
child: Column(
@@ -3,6 +3,8 @@ import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
@@ -18,21 +20,38 @@ class KeyTokenCreatedModal extends StatelessWidget {
@override
Widget build(BuildContext context) {
final acc = AppAuth();
return AlertDialog(
title: const Text('A new key was created'),
content: Container(
width: 0,
width: 9000,
height: 350,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'KeyTokenID',
values: [keytoken.keytokenID],
const BadgeDisplay(
text: "Please copy and save the token now, it cannot be retrieved later.",
icon: null,
mode: BadgeMode.warn,
),
const SizedBox(height: 16),
if (acc.userID != null)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'UserID',
values: [acc.userID!],
iconActions: [(FontAwesomeIcons.copy, null, () => _copy(acc.userID!))],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'KeyTokenID',
values: [keytoken.keytokenID],
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidInputText,
@@ -45,19 +64,12 @@ class KeyTokenCreatedModal extends StatelessWidget {
title: 'Permissions',
values: _formatPermissions(keytoken.permissions),
),
const SizedBox(height: 16),
const BadgeDisplay(
text: "Please copy and save the token now, it cannot be retrieved later.",
icon: null,
mode: BadgeMode.warn,
),
const SizedBox(height: 4),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidKey,
title: 'Token',
values: [tokenValue.substring(0, 12) + '...'],
iconActions: [(FontAwesomeIcons.copy, null, _copy)],
values: [tokenValue],
iconActions: [(FontAwesomeIcons.copy, null, () => _copy(tokenValue))],
),
],
),
@@ -89,9 +101,9 @@ class KeyTokenCreatedModal extends StatelessWidget {
return result;
}
void _copy() {
Clipboard.setData(new ClipboardData(text: tokenValue));
void _copy(String v) {
Clipboard.setData(new ClipboardData(text: v));
Toaster.info("Clipboard", 'Copied text to Clipboard');
print('================= [CLIPBOARD] =================\n${tokenValue}\n================= [/CLIPBOARD] =================');
print('================= [CLIPBOARD] =================\n${v}\n================= [/CLIPBOARD] =================');
}
}
@@ -3,10 +3,12 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_create_modal.dart';
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_created_modal.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart';
@@ -72,16 +74,21 @@ class _KeyTokenListPageState extends State<KeyTokenListPage> {
showShare: false,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, KeyToken>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<KeyToken>(
itemBuilder: (context, item, index) => KeyTokenListItem(item: item, needsReload: _fullRefresh),
child: Column(
children: [
BadgeDisplay(
text: "These are your keys.\nKeys can be used to send messages and access your account.\n\nKeys can have different sets of permissions, the Admin-Key has full-access to your account",
icon: null,
mode: BadgeMode.info,
textAlign: TextAlign.left,
closable: true,
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
hidden: !AppSettings().showInfoAlerts,
),
),
Expanded(
child: _buildList(context),
)
],
),
),
floatingActionButton: FloatingActionButton(
@@ -97,6 +104,20 @@ class _KeyTokenListPageState extends State<KeyTokenListPage> {
);
}
Widget _buildList(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, KeyToken>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<KeyToken>(
itemBuilder: (context, item, index) => KeyTokenListItem(item: item, needsReload: _fullRefresh),
),
),
);
}
void _created(KeyToken token, String tokValue) {
setState(() {
_pagingController.itemList?.insert(0, token);
@@ -78,7 +78,7 @@ class KeyTokenListItem extends StatelessWidget {
SizedBox(width: 4),
GestureDetector(
onTap: () {
Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(usedKeys: [item.keytokenID])));
Navi.push(context, () => FilteredMessageViewPage(title: item.name, alertText: 'All message sent with the key \'${item.name}\'', filter: MessageFilter(usedKeys: [item.keytokenID])));
},
child: Padding(
padding: const EdgeInsets.all(8),
@@ -40,7 +40,7 @@ class _EditKeyTokenChannelsDialogState extends State<EditKeyTokenChannelsDialog>
return AlertDialog(
title: const Text('Channels'),
content: Container(
width: 0,
width: 9000,
height: 400,
child: Column(
children: [
@@ -33,7 +33,7 @@ class _EditKeyTokenPermissionsDialogState extends State<EditKeyTokenPermissionsD
return AlertDialog(
title: const Text('Permissions'),
content: Container(
width: 0,
width: 9000,
height: 400,
child: ListView.builder(
shrinkWrap: true,
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
@@ -200,16 +201,17 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'KeyTokenID',
values: [
keytoken.keytokenID,
if (keytokenUserAccAdmin?.keytokenID == keytoken.keytokenID) '(Currently used as Admin-Token)',
if (keytokenUserAccSend?.keytokenID == keytoken.keytokenID) '(Currently used as Send-Token)',
],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'KeyTokenID',
values: [
keytoken.keytokenID,
if (keytokenUserAccAdmin?.keytokenID == keytoken.keytokenID) '(Currently used as Admin-Token)',
if (keytokenUserAccSend?.keytokenID == keytoken.keytokenID) '(Currently used as Send-Token)',
],
),
_buildNameCard(context, true),
UI.metaCard(
context: context,
@@ -230,7 +232,7 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
title: 'Messages',
values: [keytoken.messagesSent.toString()],
mainAction: () {
Navi.push(context, () => FilteredMessageViewPage(title: keytoken.name, filter: MessageFilter(usedKeys: [keytoken.keytokenID])));
Navi.push(context, () => FilteredMessageViewPage(title: keytoken.name, alertText: 'All message sent with the key \'${keytoken.name}\'', filter: MessageFilter(usedKeys: [keytoken.keytokenID])));
},
),
..._buildPermissionCard(context, true, keytoken.toPreview()),
@@ -249,12 +251,13 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'KeyTokenID',
values: [keytoken.keytokenID],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'KeyTokenID',
values: [keytoken.keytokenID],
),
_buildNameCard(context, false),
_buildOwnerCard(context, false),
..._buildPermissionCard(context, false, keytoken),
@@ -269,12 +272,20 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
future: _futureOwner.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Owner',
values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
if (AppSettings().showExtendedAttributes)
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Owner',
values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
else
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Owner',
values: [(snapshot.data?.username ?? keytokenPreview!.ownerUserID) + (isOwned ? ' (you)' : '')],
);
} else {
return UI.metaCard(
context: context,
@@ -543,6 +554,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
Toaster.info('Logout', 'Successfully deleted the key');
Navi.pop(context);
} on APIException catch (exc, trace) {
ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace);
if (!exc.toastShown) Toaster.error("Error", 'Failed to delete key');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to delete key');
ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace);
@@ -563,6 +577,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
Toaster.info("Success", "Key updated");
widget.needsReload?.call();
} on APIException catch (exc, trace) {
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
if (!exc.toastShown) Toaster.error("Error", 'Failed to update key');
} catch (exc, trace) {
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to update key');
@@ -583,6 +600,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
Toaster.info("Success", "Key updated");
widget.needsReload?.call();
} on APIException catch (exc, trace) {
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
if (!exc.toastShown) Toaster.error("Error", 'Failed to update key');
} catch (exc, trace) {
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to update key');
@@ -603,6 +623,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
Toaster.info("Success", "Key updated");
widget.needsReload?.call();
} on APIException catch (exc, trace) {
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
if (!exc.toastShown) Toaster.error("Error", 'Failed to update key');
} catch (exc, trace) {
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to update key');
+109 -59
View File
@@ -141,6 +141,8 @@ class _MessageViewPageState extends State<MessageViewPage> {
Widget _buildMessageView(BuildContext context, SCNMessage message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) {
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
var cfg = AppSettings();
final child = Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
@@ -150,6 +152,13 @@ class _MessageViewPageState extends State<MessageViewPage> {
SizedBox(height: 8),
if (message.content != null) ..._buildMessageContent(context, message),
SizedBox(height: 8),
if (cfg.showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'MessageID',
values: [message.messageID, if (message.userMessageID != null) message.userMessageID!],
),
if (message.senderName != null)
UI.metaCard(
context: context,
@@ -157,66 +166,122 @@ class _MessageViewPageState extends State<MessageViewPage> {
title: 'Sender',
values: [message.senderName!],
mainAction: () => {
Navi.push(context, () => FilteredMessageViewPage(title: message.senderName!, filter: MessageFilter(senderNames: [message.senderName!])))
Navi.push(
context,
() => FilteredMessageViewPage(
title: message.senderName!,
alertText: 'All message sent from \'${message.senderName!}\'',
filter: MessageFilter(senderNames: [message.senderName!]),
),
),
},
),
if (cfg.showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidGearCode,
title: 'Used Key',
values: [message.usedKeyID, token?.name ?? '...'],
mainAction: () {
if (message.senderUserID == userAccUserID) {
Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null));
} else {
Navi.push(
context,
() => FilteredMessageViewPage(
title: token?.name ?? message.usedKeyID,
alertText: 'All message sent with the specified key',
filter: MessageFilter(usedKeys: [message.usedKeyID]),
),
);
}
},
),
if (!cfg.showExtendedAttributes && token != null)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidGearCode,
title: 'Used Key',
values: [token.name],
mainAction: () {
if (message.senderUserID == userAccUserID) {
Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null));
} else {
Navi.push(
context,
() => FilteredMessageViewPage(
title: token.name,
alertText: 'All message sent with key \'${token.name}\'',
filter: MessageFilter(usedKeys: [message.usedKeyID]),
),
);
}
},
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidGearCode,
title: 'KeyToken',
values: [message.usedKeyID, token?.name ?? '...'],
mainAction: () {
if (message.senderUserID == userAccUserID) {
Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null));
} else {
Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID])));
}
},
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'MessageID',
values: [message.messageID, message.userMessageID ?? ''],
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidSnake,
title: 'Channel',
values: [message.channelID, channel?.displayName ?? message.channelInternalName],
values: [if (cfg.showExtendedAttributes) message.channelID, channel?.displayName ?? message.channelInternalName],
mainAction: (channel != null)
? () {
Navi.push(context, () => ChannelViewPage(channelID: channel.channelID, preloadedData: null, needsReload: null));
}
: null,
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidTimer,
title: 'Timestamp',
values: [message.timestamp],
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'User',
values: [user?.userID ?? message.senderUserID, user?.username ?? ''],
mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: user?.username ?? message.senderUserID, filter: MessageFilter(senderUserID: [message.senderUserID]))),
),
UI.metaCard(context: context, icon: FontAwesomeIcons.solidTimer, title: 'Timestamp', values: [message.timestamp]),
if (cfg.showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'User',
values: [user?.userID ?? message.senderUserID, if (user?.username != null) user?.username ?? ''],
mainAction: () => Navi.push(
context,
() => FilteredMessageViewPage(
title: user?.username ?? message.senderUserID,
alertText: 'All message sent by the specified account',
filter: MessageFilter(senderUserID: [message.senderUserID]),
),
),
),
if (!cfg.showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'User',
values: [user?.username ?? user?.userID ?? message.senderUserID],
mainAction: () => Navi.push(
context,
() => FilteredMessageViewPage(
title: user?.username ?? message.senderUserID,
alertText: 'All message sent by the specified account',
filter: MessageFilter(senderUserID: [message.senderUserID]),
),
),
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidBolt,
title: 'Priority',
values: [_prettyPrintPriority(message.priority)],
mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: "Priority ${message.priority}", filter: MessageFilter(priority: [message.priority]))),
mainAction: () => Navi.push(
context,
() => FilteredMessageViewPage(
title: "Priority ${message.priority}",
alertText: 'All message sent with priority ' + _prettyPrintPriority(message.priority),
filter: MessageFilter(priority: [message.priority]),
),
),
),
if (message.senderUserID == userAccUserID)
UI.button(
text: "Delete Message",
onPressed: () {
Toaster.info("Not Implemented", "... will be implemented in a later version"); // TODO
},
color: Colors.red[900]),
text: "Delete Message",
onPressed: () {
Toaster.info("Not Implemented", "... will be implemented in a later version"); // TODO
},
color: Colors.red[900],
),
],
),
);
@@ -234,16 +299,11 @@ class _MessageViewPageState extends State<MessageViewPage> {
thumbVisibility: false,
interactive: true,
controller: _controller,
child: SingleChildScrollView(
controller: _controller,
child: child,
),
child: SingleChildScrollView(controller: _controller, child: child),
),
);
} else {
return SingleChildScrollView(
child: child,
);
return SingleChildScrollView(child: child);
}
}
@@ -257,12 +317,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
return [
Row(
children: [
UI.channelChip(
context: context,
text: _resolveChannelName(channel, message),
margin: const EdgeInsets.fromLTRB(0, 0, 4, 0),
fontSize: 16,
),
UI.channelChip(context: context, text: _resolveChannelName(channel, message), margin: const EdgeInsets.fromLTRB(0, 0, 4, 0), fontSize: 16),
Expanded(child: SizedBox()),
Text(dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
],
@@ -310,12 +365,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
),
borderColor: (message.priority == 2) ? Colors.red[900] : null,
)
: UI.box(
context: context,
padding: const EdgeInsets.all(4),
child: Text(message.content ?? ''),
borderColor: (message.priority == 2) ? Colors.red[900] : null,
)
: UI.box(context: context, padding: const EdgeInsets.all(4), child: Text(message.content ?? ''), borderColor: (message.priority == 2) ? Colors.red[900] : null),
];
}
+15 -2
View File
@@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
@@ -283,12 +284,17 @@ class _SendRootPageState extends State<SendRootPage> {
}
try {
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgContent.text);
var content = (_msgContent.text != '') ? _msgContent.text : null;
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgTitle.text, content: content);
Toaster.success("Success", 'Message sent');
setState(() {
_msgTitle.clear();
_msgContent.clear();
});
} on APIException catch (e, stackTrace) {
if (!e.toastShown) Toaster.error("Error", 'Failed to send message: ${e.toString()}');
ApplicationLog.error('Failed to send message', trace: stackTrace);
} catch (e, stackTrace) {
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
ApplicationLog.error('Failed to send message', trace: stackTrace);
@@ -302,12 +308,19 @@ class _SendRootPageState extends State<SendRootPage> {
}
try {
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgContent.text, channel: _channelName.text, senderName: _senderName.text, priority: _priority);
var content = (_msgContent.text != '') ? _msgContent.text : null;
var channel = (_channelName.text != '') ? _channelName.text : null;
var sender = (_senderName.text != '') ? _senderName.text : null;
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgTitle.text, content: content, channel: channel, senderName: sender, priority: _priority);
Toaster.success("Success", 'Message sent');
setState(() {
_msgTitle.clear();
_msgContent.clear();
});
} on APIException catch (e, stackTrace) {
if (!e.toastShown) Toaster.error("Error", 'Failed to send message: ${e.toString()}');
ApplicationLog.error('Failed to send message', trace: stackTrace);
} catch (e, stackTrace) {
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
ApplicationLog.error('Failed to send message', trace: stackTrace);
+30 -9
View File
@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/sender_list/sender_list_item.dart';
@@ -69,16 +71,35 @@ class _SenderListPageState extends State<SenderListPage> {
showShare: false,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, SenderNameStatistics>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<SenderNameStatistics>(
itemBuilder: (context, item, index) => SenderListItem(item: item),
child: Column(
children: [
BadgeDisplay(
text: "All sender used to send messages to this account",
icon: null,
mode: BadgeMode.info,
textAlign: TextAlign.left,
closable: true,
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
hidden: !AppSettings().showInfoAlerts,
),
),
Expanded(
child: _buildList(context),
)
],
),
),
);
}
Widget _buildList(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, SenderNameStatistics>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<SenderNameStatistics>(
itemBuilder: (context, item, index) => SenderListItem(item: item),
),
),
);
@@ -30,7 +30,7 @@ class SenderListItem extends StatelessWidget {
color: Theme.of(context).cardTheme.color,
child: InkWell(
onTap: () {
Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name])));
Navi.push(context, () => FilteredMessageViewPage(title: item.name, alertText: 'All message sent from \'${item.name!}\'', filter: MessageFilter(senderNames: [item.name])));
},
child: Padding(
padding: const EdgeInsets.all(8),
@@ -71,7 +71,7 @@ class SenderListItem extends StatelessWidget {
SizedBox(width: 4),
GestureDetector(
onTap: () {
Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name])));
Navi.push(context, () => FilteredMessageViewPage(title: item.name, alertText: 'All message sent from \'${item.name!}\'', filter: MessageFilter(senderNames: [item.name])));
},
child: Padding(
padding: const EdgeInsets.all(8),
+13 -1
View File
@@ -146,6 +146,18 @@ class _SettingsRootPageState extends State<SettingsRootPage> {
title: Text('Refresh messages on app resume'),
onToggle: (value) => AppSettings().update((p) => p.alwaysBackgroundRefreshMessageListOnLifecycleResume = !p.alwaysBackgroundRefreshMessageListOnLifecycleResume),
),
SettingsTile.switchTile(
initialValue: cfg.showInfoAlerts,
leading: Icon(FontAwesomeIcons.solidCircleInfo),
title: Text('Show various helpful info boxes'),
onToggle: (value) => AppSettings().update((p) => p.showInfoAlerts = !p.showInfoAlerts),
),
SettingsTile.switchTile(
initialValue: cfg.showExtendedAttributes,
leading: Icon(FontAwesomeIcons.solidSheetPlastic),
title: Text('Show all attributes of entities'),
onToggle: (value) => AppSettings().update((p) => p.showExtendedAttributes = !p.showExtendedAttributes),
),
],
),
SettingsSection(
@@ -177,7 +189,7 @@ class _SettingsRootPageState extends State<SettingsRootPage> {
value: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(GitStamp.sha.substring(0, 7) + ' +' + Globals().buildNumber),
Text('${GitStamp.sha.substring(0, GitStamp.sha.length.clamp(0, 7))} +${Globals().buildNumber}'),
Text("( " + cfg.dateFormat.dateFormat().format(DateTime.parse(GitStamp.buildDateTime).toLocal()) + " )", style: TextStyle(fontStyle: FontStyle.italic)),
],
),
@@ -2,15 +2,41 @@ import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
import 'package:simplecloudnotifier/components/filter_chips/filter_chips.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/subscription_list/subscription_list_item.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
enum SubscriptionListFilter {
ALL,
INACTIVE,
OWN,
EXTERNAL,
INCOMING;
SubscriptionFilter toAPIFilter() {
switch (this) {
case ALL:
return SubscriptionFilter.ALL;
case INACTIVE:
return SubscriptionFilter.OWNED_INACTIVE;
case OWN:
return SubscriptionFilter.OWNED_ACTIVE;
case EXTERNAL:
return SubscriptionFilter.EXTERNAL_ALL;
case INCOMING:
return SubscriptionFilter.INCOMING_ALL;
}
}
}
class SubscriptionListPage extends StatefulWidget {
const SubscriptionListPage({super.key});
@@ -24,6 +50,8 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
final userCache = Map<String, UserPreview>();
final channelCache = Map<String, ChannelPreview>();
SubscriptionListFilter filter = SubscriptionListFilter.ALL;
@override
void initState() {
super.initState();
@@ -58,7 +86,7 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
}
try {
final items = (await APIClient.getSubscriptionList(acc)).toList();
final items = (await APIClient.getSubscriptionList(acc, filter.toAPIFilter())).toList();
items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated));
@@ -93,16 +121,51 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
showShare: false,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, Subscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Subscription>(
itemBuilder: (context, item, index) => SubscriptionListItem(item: item, userCache: userCache, channelCache: channelCache, needsReload: fullRefresh),
child: Column(
children: [
BadgeDisplay(
text: "These are subscriptions to individual Channels\n\nThey contain to your own channels, subscriptions to foreign channels and subscriptions of other users to your channels (active and requested).",
icon: null,
mode: BadgeMode.info,
textAlign: TextAlign.left,
closable: true,
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
hidden: !AppSettings().showInfoAlerts,
),
),
FilterChips<SubscriptionListFilter>(
options: [
(SubscriptionListFilter.ALL, 'All'),
(SubscriptionListFilter.OWN, 'Own'),
(SubscriptionListFilter.EXTERNAL, 'External'),
(SubscriptionListFilter.INCOMING, 'Incoming'),
(SubscriptionListFilter.INACTIVE, 'Inactive'),
],
value: filter,
onChanged: (newFilter) {
setState(() {
filter = newFilter;
_pagingController.refresh();
});
},
),
Expanded(
child: _buildList(context),
)
],
),
),
);
}
Widget _buildList(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, Subscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Subscription>(
itemBuilder: (context, item, index) => SubscriptionListItem(item: item, userCache: userCache, channelCache: channelCache, needsReload: fullRefresh),
),
),
);
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
@@ -168,12 +169,13 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'SubscriptionID',
values: [subscription.subscriptionID],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'SubscriptionID',
values: [subscription.subscriptionID],
),
_buildChannelOwnerCard(context, subscription),
_buildSubscriberCard(context, subscription),
_buildChannelCard(context, subscription),
@@ -201,12 +203,13 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'SubscriptionID',
values: [subscription.subscriptionID],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'SubscriptionID',
values: [subscription.subscriptionID],
),
_buildChannelOwnerCard(context, subscription),
_buildSubscriberCard(context, subscription),
_buildChannelCard(context, subscription),
@@ -236,12 +239,13 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'SubscriptionID',
values: [subscription.subscriptionID],
),
if (AppSettings().showExtendedAttributes)
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'SubscriptionID',
values: [subscription.subscriptionID],
),
_buildChannelOwnerCard(context, subscription),
_buildSubscriberCard(context, subscription),
_buildChannelCard(context, subscription),
@@ -271,12 +275,20 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
future: _futureChannelOwner.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Channel Owner',
values: [subscription.channelOwnerUserID + (isSelf ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
if (AppSettings().showExtendedAttributes)
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Channel Owner',
values: [subscription.channelOwnerUserID + (isSelf ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
else
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Channel Owner',
values: [(snapshot.data?.username ?? subscription.channelOwnerUserID) + (isSelf ? ' (you)' : '')],
);
} else {
return UI.metaCard(
context: context,
@@ -298,12 +310,20 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
future: _futureSubscriber.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Subscriber',
values: [subscription.subscriberUserID + (isSelf ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
if (AppSettings().showExtendedAttributes)
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Subscriber',
values: [subscription.subscriberUserID + (isSelf ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
else
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Subscriber',
values: [(snapshot.data?.username ?? subscription.subscriberUserID) + (isSelf ? ' (you)' : '')],
);
} else {
return UI.metaCard(
context: context,
@@ -321,13 +341,22 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
future: _futureChannel.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidSnake,
title: 'Channel',
values: [subscription.channelID, snapshot.data!.displayName],
mainAction: () => Navi.push(context, () => ChannelViewPage(channelID: subscription.channelID, preloadedData: null, needsReload: null)),
);
if (AppSettings().showExtendedAttributes)
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidSnake,
title: 'Channel',
values: [subscription.channelID, snapshot.data!.displayName],
mainAction: () => Navi.push(context, () => ChannelViewPage(channelID: subscription.channelID, preloadedData: null, needsReload: null)),
);
else
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidSnake,
title: 'Channel',
values: [snapshot.data!.displayName],
mainAction: () => Navi.push(context, () => ChannelViewPage(channelID: subscription.channelID, preloadedData: null, needsReload: null)),
);
} else {
return UI.metaCard(
context: context,
@@ -407,6 +436,9 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully confirmed');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to confirm subscription');
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to confirm subscription');
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
@@ -429,6 +461,9 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
Toaster.success("Success", 'Unsubscribed from channel');
Navi.pop(context);
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -447,6 +482,9 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
await _initStateAsync(false);
Toaster.success("Success", 'Unsubscribed from channel');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -465,6 +503,9 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
await _initStateAsync(false);
Toaster.success("Success", 'Subscribed to channel');
} on APIException catch (exc, trace) {
if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
+6 -2
View File
@@ -99,7 +99,7 @@ class AppAuth extends ChangeNotifier implements TokenSource {
final user = await APIClient.getUser(DirectTokenSource(oldUserID, oldUserKey), oldUserID);
final client = await APIClient.addClient(DirectTokenSource(oldUserID, oldUserKey), fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType);
final client = await APIClient.addClient(DirectTokenSource(oldUserID, oldUserKey), fcmToken, Globals().deviceModel, Globals().version, Globals().nameForClient(), Globals().clientType);
set(user, client, oldUserKey, newTokenSend.token);
@@ -232,7 +232,7 @@ class AppAuth extends ChangeNotifier implements TokenSource {
return _user?.$1;
}
Future<Client?> loadClient({bool force = false, Duration? forceIfOlder = null}) async {
Future<Client?> loadClient({bool force = false, Duration? forceIfOlder = null, bool onlyCached = false}) async {
if (forceIfOlder != null && _client != null && _client!.$2.difference(DateTime.now()) > forceIfOlder) {
force = true;
}
@@ -245,6 +245,10 @@ class AppAuth extends ChangeNotifier implements TokenSource {
throw Exception('Not authenticated');
}
if (onlyCached) {
return null;
}
try {
final client = await APIClient.getClient(this, _clientID!);
+8
View File
@@ -56,6 +56,8 @@ class AppSettings extends ChangeNotifier {
bool alwaysBackgroundRefreshMessageListOnLifecycleResume = true;
AppSettingsDateFormat dateFormat = AppSettingsDateFormat.ISO;
int messagePreviewLength = 3;
bool showInfoAlerts = true;
bool showExtendedAttributes = false;
AppNotificationSettings notification0 = AppNotificationSettings();
AppNotificationSettings notification1 = AppNotificationSettings();
@@ -80,6 +82,8 @@ class AppSettings extends ChangeNotifier {
alwaysBackgroundRefreshMessageListOnLifecycleResume = true;
dateFormat = AppSettingsDateFormat.ISO;
messagePreviewLength = 3;
showInfoAlerts = true;
showExtendedAttributes = false;
notification0 = AppNotificationSettings();
notification1 = AppNotificationSettings();
@@ -97,6 +101,8 @@ class AppSettings extends ChangeNotifier {
alwaysBackgroundRefreshMessageListOnLifecycleResume = Globals().sharedPrefs.getBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume') ?? alwaysBackgroundRefreshMessageListOnLifecycleResume;
dateFormat = AppSettingsDateFormat.parse(Globals().sharedPrefs.getString('settings.dateFormat')) ?? dateFormat;
messagePreviewLength = Globals().sharedPrefs.getInt('settings.messagePreviewLength') ?? messagePreviewLength;
showInfoAlerts = Globals().sharedPrefs.getBool('settings.showInfoAlerts') ?? showInfoAlerts;
showExtendedAttributes = Globals().sharedPrefs.getBool('settings.showExtendedAttributes') ?? showExtendedAttributes;
notification0 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification0');
notification1 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification1');
@@ -112,6 +118,8 @@ class AppSettings extends ChangeNotifier {
await Globals().sharedPrefs.setBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume', alwaysBackgroundRefreshMessageListOnLifecycleResume);
await Globals().sharedPrefs.setString('settings.dateFormat', dateFormat.key);
await Globals().sharedPrefs.setInt('settings.messagePreviewLength', messagePreviewLength);
await Globals().sharedPrefs.setBool('settings.showInfoAlerts', showInfoAlerts);
await Globals().sharedPrefs.setBool('settings.showExtendedAttributes', showExtendedAttributes);
await notification0.save(Globals().sharedPrefs, 'settings.notification0');
await notification1.save(Globals().sharedPrefs, 'settings.notification1');
+8
View File
@@ -92,4 +92,12 @@ class Globals {
Future<bool> setPrefFCMToken(String value) {
return sharedPrefs.setString("fcm.token", value);
}
String nameForClient() {
if (this.deviceName.isNotEmpty) {
return this.deviceName;
} else {
return this.hostname;
}
}
}
+34
View File
@@ -28,6 +28,40 @@ class UIDialogs {
);
}
static Future<String?> showUsernameRequiredDialog(BuildContext context) {
var _textFieldController = TextEditingController();
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Username Required'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Please set a public username to subscribe to channels from other users.'),
SizedBox(height: 16),
TextField(
autofocus: true,
controller: _textFieldController,
decoration: InputDecoration(hintText: 'Enter username'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(_textFieldController.text),
child: Text('OK'),
),
],
),
);
}
static Future<bool> showConfirmDialog(BuildContext context, String title, {String? text, String? okText, String? cancelText}) {
return showDialog<bool>(
context: context,
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
+119 -5
View File
@@ -21,12 +21,14 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
30051E098E5DDBD2E1D16F95 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 260FDBA879E00B3BD57336B6 /* Pods_RunnerTests.framework */; };
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
D6E1D80C6733E06BC2484012 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71450153D8FB1F125629A01B /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -60,11 +62,12 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
260FDBA879E00B3BD57336B6 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* simplecloudnotifier.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "simplecloudnotifier.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10ED2044A3C60003C045 /* simplecloudnotifier.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = simplecloudnotifier.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@@ -76,8 +79,15 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
71450153D8FB1F125629A01B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
71DA993D9EDD2A4A54234FF2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9496FF8D8B46D4619EDAA5EF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
B9F3F9EB590C25EBDAE05E03 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
BF6145095420650A6CCA9EE3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
C0E65636E0666802F744A5E3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
C71F9C04DE64EB26EF8E0E58 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -85,6 +95,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
30051E098E5DDBD2E1D16F95 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -92,12 +103,27 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D6E1D80C6733E06BC2484012 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
25379216233BB0105FDA3DD4 /* Pods */ = {
isa = PBXGroup;
children = (
C0E65636E0666802F744A5E3 /* Pods-Runner.debug.xcconfig */,
71DA993D9EDD2A4A54234FF2 /* Pods-Runner.release.xcconfig */,
B9F3F9EB590C25EBDAE05E03 /* Pods-Runner.profile.xcconfig */,
C71F9C04DE64EB26EF8E0E58 /* Pods-RunnerTests.debug.xcconfig */,
BF6145095420650A6CCA9EE3 /* Pods-RunnerTests.release.xcconfig */,
9496FF8D8B46D4619EDAA5EF /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@@ -125,6 +151,7 @@
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
25379216233BB0105FDA3DD4 /* Pods */,
);
sourceTree = "<group>";
};
@@ -175,6 +202,8 @@
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
71450153D8FB1F125629A01B /* Pods_Runner.framework */,
260FDBA879E00B3BD57336B6 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -186,6 +215,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
DA445DDFEB72AB2901C75BCA /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
@@ -204,11 +234,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
27E4C9B69EF1878B4DAFB474 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
A4B4F505F9548B10CBCD5F0A /* [CP] Embed Pods Frameworks */,
B2787524EFDFEFCFFDA32C4E /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -227,7 +260,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
@@ -290,6 +323,28 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
27E4C9B69EF1878B4DAFB474 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -328,6 +383,62 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
A4B4F505F9548B10CBCD5F0A /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
B2787524EFDFEFCFFDA32C4E /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
DA445DDFEB72AB2901C75BCA /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -379,6 +490,7 @@
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = C71F9C04DE64EB26EF8E0E58 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@@ -393,6 +505,7 @@
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BF6145095420650A6CCA9EE3 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@@ -407,6 +520,7 @@
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9496FF8D8B46D4619EDAA5EF /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@@ -457,7 +571,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -536,7 +650,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@@ -583,7 +697,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -59,6 +59,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
+5 -1
View File
@@ -1,9 +1,13 @@
import Cocoa
import FlutterMacOS
@NSApplicationMain
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
+130 -151
View File
@@ -5,23 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
url: "https://pub.dev"
source: hosted
version: "76.0.0"
version: "67.0.0"
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: de9ecbb3ddafd446095f7e833c853aff2fa1682b017921fe63a833f9d6f0e422
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
url: "https://pub.dev"
source: hosted
version: "1.3.54"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
version: "1.3.59"
action_slider:
dependency: "direct main"
description:
@@ -34,18 +29,18 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
url: "https://pub.dev"
source: hosted
version: "6.11.0"
version: "6.4.1"
archive:
dependency: transitive
description:
name: archive
sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12"
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.5"
version: "4.0.7"
args:
dependency: transitive
description:
@@ -66,18 +61,18 @@ packages:
dependency: transitive
description:
name: asn1lib
sha256: e02d018628c870ef2d7f03e33f9ad179d89ff6ec52ca6c56bcb80bcef979867f
sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024"
url: "https://pub.dev"
source: hosted
version: "1.6.2"
version: "1.6.5"
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.12.0"
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
@@ -90,10 +85,10 @@ packages:
dependency: transitive
description:
name: build
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.1"
build_config:
dependency: transitive
description:
@@ -106,34 +101,34 @@ packages:
dependency: transitive
description:
name: build_daemon
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d"
url: "https://pub.dev"
source: hosted
version: "4.0.4"
version: "4.1.0"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
url: "https://pub.dev"
source: hosted
version: "2.4.4"
version: "2.4.2"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
url: "https://pub.dev"
source: hosted
version: "2.4.15"
version: "2.4.13"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
url: "https://pub.dev"
source: hosted
version: "8.0.0"
version: "7.3.2"
built_collection:
dependency: transitive
description:
@@ -146,10 +141,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
url: "https://pub.dev"
source: hosted
version: "8.9.5"
version: "8.12.0"
characters:
dependency: transitive
description:
@@ -162,10 +157,10 @@ packages:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "2.0.4"
cli_util:
dependency: transitive
description:
@@ -186,10 +181,10 @@ packages:
dependency: transitive
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
url: "https://pub.dev"
source: hosted
version: "4.10.1"
version: "4.11.0"
collection:
dependency: transitive
description:
@@ -210,18 +205,18 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
version: "0.3.5"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
@@ -234,10 +229,10 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
url: "https://pub.dev"
source: hosted
version: "2.3.8"
version: "2.3.6"
dbus:
dependency: transitive
description:
@@ -250,18 +245,18 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: "306b78788d1bb569edb7c55d622953c2414ca12445b41c9117963e03afc5c513"
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
url: "https://pub.dev"
source: hosted
version: "11.3.3"
version: "11.5.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
url: "https://pub.dev"
source: hosted
version: "7.0.2"
version: "7.0.3"
encrypt:
dependency: transitive
description:
@@ -282,10 +277,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
ffi:
dependency: transitive
description:
@@ -306,50 +301,50 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: "017d17d9915670e6117497e640b2859e0b868026ea36bf3a57feb28c3b97debe"
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"
url: "https://pub.dev"
source: hosted
version: "3.13.0"
version: "3.15.2"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64
url: "https://pub.dev"
source: hosted
version: "5.4.0"
version: "6.0.2"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "129a34d1e0fb62e2b488d988a1fc26cc15636357e50944ffee2862efe8929b23"
sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
url: "https://pub.dev"
source: hosted
version: "2.22.0"
version: "2.24.1"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "5f8918848ee0c8eb172fc7698619b2bcd7dda9ade8b93522c6297dd8f9178356"
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
url: "https://pub.dev"
source: hosted
version: "15.2.5"
version: "15.2.10"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "0bbea00680249595fc896e7313a2bd90bd55be6e0abbe8b9a39d81b6b306acb6"
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
url: "https://pub.dev"
source: hosted
version: "4.6.5"
version: "4.6.10"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: ffb392ce2a7e8439cd0a9a80e3c702194e73c927e5c7b4f0adf6faa00b245b17
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
url: "https://pub.dev"
source: hosted
version: "3.10.5"
version: "3.10.10"
fixnum:
dependency: transitive
description:
@@ -362,10 +357,10 @@ packages:
dependency: transitive
description:
name: fl_chart
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
url: "https://pub.dev"
source: hosted
version: "0.69.2"
version: "0.70.2"
flutter:
dependency: "direct main"
description: flutter
@@ -375,18 +370,18 @@ packages:
dependency: "direct main"
description:
name: flutter_launcher_icons
sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.3"
version: "0.14.4"
flutter_lazy_indexed_stack:
dependency: "direct main"
description:
name: flutter_lazy_indexed_stack
sha256: e5529b516890475465c8c34b23d611e9c46b23c745c08edf471d1b6f899f5c9f
sha256: "3e905c0f130538f686e4e07bb8d0bc0dee6890366c65da199fb356aab52e0bff"
url: "https://pub.dev"
source: hosted
version: "0.0.6"
version: "0.0.7"
flutter_lints:
dependency: "direct dev"
description:
@@ -456,10 +451,10 @@ packages:
dependency: "direct main"
description:
name: git_stamp
sha256: eddda29d15136503af57b5d927393fab2e7fe9660d4dc9ae418eb69c3f542c30
sha256: "25b123a0fd214c5d87f2a32f990771da76d2f24d66f6767cc190e174e3670fdd"
url: "https://pub.dev"
source: hosted
version: "5.10.0"
version: "5.10.1"
glob:
dependency: transitive
description:
@@ -504,10 +499,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.5.0"
http_multi_server:
dependency: transitive
description:
@@ -528,10 +523,10 @@ packages:
dependency: transitive
description:
name: iconsax_flutter
sha256: "95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d"
sha256: d14b4cec8586025ac15276bdd40f6eea308cb85748135965bb6255f14beb2564
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.1"
image:
dependency: transitive
description:
@@ -552,10 +547,10 @@ packages:
dependency: transitive
description:
name: in_app_purchase_android
sha256: fd76e5612da6facadcfe8a3477da092908227260a9f6ec7db9a66dd989c69b02
sha256: "2d0e2d27b93bd7457526419d7feb07baf5608d733ecf6cdcd2e3b03ea7248915"
url: "https://pub.dev"
source: hosted
version: "0.4.0+2"
version: "0.4.0+6"
in_app_purchase_platform_interface:
dependency: transitive
description:
@@ -568,10 +563,10 @@ packages:
dependency: transitive
description:
name: in_app_purchase_storekit
sha256: "9c2b438aa8ef95ac1c1f5ab1aaace8d6edd0ba3745b8b8df832f7baa2e7492f7"
sha256: bfdb8d1859b6d19a55aba1046e3a860c631b6e96d36275a358e1caf8b62cfbde
url: "https://pub.dev"
source: hosted
version: "0.4.1"
version: "0.4.6+1"
infinite_scroll_pagination:
dependency: "direct main"
description:
@@ -616,26 +611,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -652,14 +647,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
matcher:
dependency: transitive
description:
@@ -696,10 +683,10 @@ packages:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "9cb9e371ee9b5b548714f9ab5fd33b530d799745c83d5729ecd1e8ab2935dbd1"
sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173"
url: "https://pub.dev"
source: hosted
version: "6.0.7"
version: "6.0.11"
mutex:
dependency: "direct main"
description:
@@ -728,18 +715,18 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
url: "https://pub.dev"
source: hosted
version: "8.3.0"
version: "8.3.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.1"
path:
dependency: "direct main"
description:
@@ -760,18 +747,18 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16
url: "https://pub.dev"
source: hosted
version: "2.2.16"
version: "2.2.20"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.3"
path_provider_linux:
dependency: transitive
description:
@@ -808,10 +795,10 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "7.0.1"
platform:
dependency: transitive
description:
@@ -840,26 +827,26 @@ packages:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.1"
version: "6.0.3"
provider:
dependency: "direct main"
description:
name: provider
sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310"
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
@@ -928,18 +915,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: c2c8c46297b5d6a80bed7741ec1f2759742c77d272f1a1698176ae828f8e1a18
sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713"
url: "https://pub.dev"
source: hosted
version: "2.4.9"
version: "2.4.15"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "2.5.5"
shared_preferences_linux:
dependency: transitive
description:
@@ -984,10 +971,10 @@ packages:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "2.0.1"
sky_engine:
dependency: transitive
description: flutter
@@ -1025,14 +1012,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
@@ -1077,10 +1056,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.6"
timezone:
dependency: transitive
description:
@@ -1101,10 +1080,10 @@ packages:
dependency: "direct main"
description:
name: toastification
sha256: "9713989549d60754fd0522425d1251501919cfb7bab4ffbbb36ef40de5ea72b9"
sha256: "69db2bff425b484007409650d8bcd5ed1ce2e9666293ece74dcd917dacf23112"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
typed_data:
dependency: transitive
description:
@@ -1117,26 +1096,26 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.1"
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9"
url: "https://pub.dev"
source: hosted
version: "6.3.15"
version: "6.3.24"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9"
url: "https://pub.dev"
source: hosted
version: "6.3.3"
version: "6.3.5"
url_launcher_linux:
dependency: transitive
description:
@@ -1149,10 +1128,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
version: "3.2.4"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -1165,10 +1144,10 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
@@ -1181,34 +1160,34 @@ packages:
dependency: "direct main"
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.1"
version: "4.5.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "14.3.1"
version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.4"
web:
dependency: transitive
description:
@@ -1221,26 +1200,26 @@ packages:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.12.0"
version: "5.15.0"
win32_registry:
dependency: transitive
description:
@@ -1269,10 +1248,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
version: "6.6.1"
yaml:
dependency: transitive
description:
@@ -1282,5 +1261,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"
+2 -2
View File
@@ -2,10 +2,10 @@ name: simplecloudnotifier
description: "Receive push messages"
publish_to: 'none'
version: 2.0.1+492
version: 2.2.2+544
environment:
sdk: '>=3.2.6 <4.0.0'
sdk: '>=3.9.0 <4.0.0'
dependencies:
flutter:
+7
View File
@@ -17,11 +17,18 @@ simple_cloud_notifier-*.sql
identifier.sqlite
.idea/dataSources.xml
.idea/copilot*
.idea/go.imports.xml
.swaggobin
scn_send.sh
swagger/swagger.json
swagger/swagger.yaml
**/*_gen.go
##############
+1
View File
@@ -26,6 +26,7 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists
COPY --from=builder /buildsrc/_build/scn_backend /app/server
COPY --from=builder /buildsrc/DOCKER_GIT_INFO /DOCKER_GIT_INFO
RUN mkdir /data
+2
View File
@@ -58,6 +58,8 @@ swagger-setup:
swagger: swagger-setup
".swaggobin/swag_$(SWAGGO_VERSION)" init -generalInfo ./api/router.go --propertyStrategy camelcase --output ./swagger/ --outputTypes "json,yaml"
generate: dgi enums ids swagger
pygmentize: website/scn_send.html
website/scn_send.html: ../scn_send.sh
+7 -6
View File
@@ -1,19 +1,20 @@
package handler
import (
"database/sql"
"errors"
"fmt"
"net/http"
"strings"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/mathext"
"net/http"
"strings"
)
// ListChannels swaggerdoc
@@ -487,7 +488,7 @@ func (h APIHandler) ListChannelMessages(pctx ginext.PreContext) ginext.HTTPRespo
// @Failure 404 {object} ginresp.apiError "channel not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/users/{uid}/channels/{cid} [PATCH]
// @Router /api/v2/users/{uid}/channels/{cid} [DELETE]
func (h APIHandler) DeleteChannel(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
+59
View File
@@ -271,6 +271,65 @@ func (h APIHandler) GetMessage(pctx ginext.PreContext) ginext.HTTPResponse {
})
}
// ListMessageDeliveries swaggerdoc
//
// @Summary List deliveries for a message
// @Description The user must own the channel and request the resource with the ADMIN Key
// @ID api-messages-deliveries
// @Tags API-v2
//
// @Param mid path string true "MessageID"
//
// @Success 200 {object} handler.ListMessageDeliveries.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/messages/{mid}/deliveries [GET]
func (h APIHandler) ListMessageDeliveries(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct {
MessageID models.MessageID `uri:"mid" binding:"entityid"`
}
type response struct {
Deliveries []models.Delivery `json:"deliveries"`
}
var u uri
ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
if permResp := ctx.CheckPermissionAny(); permResp != nil {
return *permResp
}
msg, err := h.database.GetMessage(ctx, u.MessageID, false)
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)
}
// User must own the channel and have admin key
if permResp := ctx.CheckPermissionUserAdmin(msg.ChannelOwnerUserID); permResp != nil {
return *permResp
}
deliveries, err := h.database.ListDeliveriesOfMessage(ctx, msg.MessageID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query deliveries", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, response{Deliveries: deliveries}))
})
}
// DeleteMessage swaggerdoc
//
// @Summary Delete a single message
+75 -12
View File
@@ -1,14 +1,15 @@
package handler
import (
"database/sql"
"errors"
"net/http"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"net/http"
)
// GetUserPreview swaggerdoc
@@ -52,7 +53,7 @@ func (h APIHandler) GetUserPreview(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, user.JSONPreview()))
return finishSuccess(ginext.JSON(http.StatusOK, user.Preview()))
})
}
@@ -92,13 +93,13 @@ func (h APIHandler) GetChannelPreview(pctx ginext.PreContext) ginext.HTTPRespons
userid := *ctx.GetPermissionUserID()
channel, err := h.database.GetChannelByID(ctx, u.ChannelID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel, err := h.database.GetChannelByIDOpt(ctx, u.ChannelID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if channel == nil {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
sub, err := h.database.GetSubscriptionBySubscriber(ctx, userid, channel.ChannelID)
if err != nil {
@@ -161,13 +162,13 @@ func (h APIHandler) GetUserKeyPreview(pctx ginext.PreContext) ginext.HTTPRespons
// Query by token.token
keytoken, err := h.database.GetKeyTokenByToken(ctx, u.KeyID)
if keytoken == nil {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
}
keytoken, err := h.database.GetKeyTokenByTokenOpt(ctx, u.KeyID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
}
if keytoken == nil {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, keytoken.Preview()))
@@ -175,3 +176,65 @@ func (h APIHandler) GetUserKeyPreview(pctx ginext.PreContext) ginext.HTTPRespons
})
}
// GetClientPreview swaggerdoc
//
// @Summary Get a client (similar to api-clients-get, but can be called from anyone and only returns a subset of fields)
// @ID api-clients-get-preview
// @Tags API-v2
//
// @Param cid path string true "ClientID"
//
// @Success 200 {object} handler.GetClientPreview.response
//
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "client not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/preview/clients/{cid} [GET]
func (h APIHandler) GetClientPreview(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct {
ClientID models.ClientID `uri:"cid" binding:"entityid"`
}
type response struct {
Client models.ClientPreview `json:"client"`
User models.UserPreview `json:"user"`
}
var u uri
ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
if permResp := ctx.CheckPermissionAny(); permResp != nil {
return *permResp
}
client, err := h.database.GetClientByIDOpt(ctx, u.ClientID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
}
if client == nil {
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
}
user, err := h.database.GetUser(ctx, client.UserID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, response{
Client: client.Preview(),
User: user.Preview(),
}))
})
}
+4 -3
View File
@@ -1,12 +1,13 @@
package handler
import (
"net/http"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"net/http"
)
// ListUserSenderNames swaggerdoc
@@ -17,13 +18,13 @@ import (
//
// @Param uid path string true "UserID"
//
// @Success 200 {object} handler.ListUserKeys.response
// @Success 200 {object} handler.ListUserSenderNames.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/users/{uid}/keys [GET]
// @Router /api/v2/users/{uid}/sender-names [GET]
func (h APIHandler) ListUserSenderNames(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
+9 -7
View File
@@ -1,16 +1,17 @@
package handler
import (
"database/sql"
"errors"
"net/http"
"strings"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"net/http"
"strings"
)
// ListUserSubscriptions swaggerdoc
@@ -39,8 +40,9 @@ import (
// @ID api-user-subscriptions-list
// @Tags API-v2
//
// @Param uid path string true "UserID"
// @Param selector query string true "Filter subscriptions (default: outgoing_all)" Enums(outgoing_all, outgoing_confirmed, outgoing_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
// @Param uid path string true "UserID"
//
// @Param query_data query handler.ListUserSubscriptions.query false " "
//
// @Success 200 {object} handler.ListUserSubscriptions.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
@@ -360,7 +362,7 @@ func (h APIHandler) CreateSubscription(pctx ginext.PreContext) ginext.HTTPRespon
} else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil {
outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID)
outchannel, err := h.database.GetChannelByIDOpt(ctx, *b.ChannelID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
+12 -11
View File
@@ -1,6 +1,11 @@
package handler
import (
"database/sql"
"errors"
"fmt"
"net/http"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
@@ -8,13 +13,9 @@ import (
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"net/http"
)
type CompatHandler struct {
@@ -90,7 +91,7 @@ func (h CompatHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", 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)
okResp, errResp := h.app.SendMessage(g, ctx, data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil)
if errResp != nil {
return *errResp
} else {
@@ -304,7 +305,7 @@ func (h CompatHandler) Info(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
@@ -416,7 +417,7 @@ func (h CompatHandler) Ack(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
@@ -522,7 +523,7 @@ func (h CompatHandler) Requery(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
@@ -643,7 +644,7 @@ func (h CompatHandler) Update(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
@@ -777,7 +778,7 @@ func (h CompatHandler) Expand(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
@@ -900,7 +901,7 @@ func (h CompatHandler) Upgrade(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
+73 -16
View File
@@ -1,16 +1,17 @@
package handler
import (
"fmt"
"net/http"
"time"
"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"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"net/http"
"time"
)
type ExternalHandler struct {
@@ -27,8 +28,10 @@ func NewExternalHandler(app *logic.Application) ExternalHandler {
// 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
// @Summary Send a new message (uses uptime-kuma notification schema)
// @Description Set necessary parameter via query (key, channel etc.), title+message are build from uptime-kuma payload
// @Description You can specify different channels/priorities for [up] and [down] notifications
//
// @Tags External
//
// @Param query_data query handler.UptimeKuma.query false " "
@@ -36,22 +39,21 @@ func NewExternalHandler(app *logic.Application) ExternalHandler {
//
// @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 401 {object} ginresp.apiError "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(pctx ginext.PreContext) ginext.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"`
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 {
@@ -125,7 +127,62 @@ func (h ExternalHandler) UptimeKuma(pctx ginext.PreContext) ginext.HTTPResponse
priority = q.PriorityDown
}
okResp, errResp := h.app.SendMessage(g, ctx, q.UserID, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName)
okResp, errResp := h.app.SendMessage(g, ctx, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName)
if errResp != nil {
return *errResp
}
return finishSuccess(ginext.JSON(http.StatusOK, response{
MessageID: okResp.Message.MessageID,
}))
})
}
// Shoutrrr swaggerdoc
//
// @Summary Send a new message (uses shoutrrr generic:// format=json schema)
// @Description Set necessary parameter via query (key, channel etc.), title+message are set via the shoutrrr payload
// @Description Use the shoutrrr format `generic://{{url}}?template=json`
//
// @Tags External
//
// @Param query_data query handler.Shoutrrr.query false " "
// @Param post_body body handler.Shoutrrr.body false " "
//
// @Success 200 {object} handler.Shoutrrr.response
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError "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) Shoutrrr(pctx ginext.PreContext) ginext.HTTPResponse {
type query struct {
KeyToken *string `form:"key" example:"P3TNH8mvv14fm"`
Channel *string `form:"channel"`
Priority *int `form:"priority"`
SenderName *string `form:"senderName"`
}
type body struct {
Title string `json:"title"`
Message string `json:"message"`
}
type response struct {
MessageID models.MessageID `json:"message_id"`
}
var b body
var q query
ctx, g, errResp := pctx.Query(&q).Body(&b).Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
okResp, errResp := h.app.SendMessage(g, ctx, q.KeyToken, q.Channel, &b.Title, &b.Message, q.Priority, nil, nil, q.SenderName)
if errResp != nil {
return *errResp
}
+12 -12
View File
@@ -1,6 +1,8 @@
package handler
import (
"net/http"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic"
@@ -8,7 +10,6 @@ import (
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"net/http"
)
type SendMessageResponse struct {
@@ -42,7 +43,7 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
//
// @Success 200 {object} handler.SendMessage.response
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError "The user_id was not found or the user_key is wrong"
// @Failure 401 {object} ginresp.apiError "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"
//
@@ -50,15 +51,14 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
// @Router /send [POST]
func (h MessageHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse {
type combined struct {
UserID *models.UserID `json:"user_id" form:"user_id" example:"7725" `
KeyToken *string `json:"key" form:"key" example:"P3TNH8mvv14fm" `
Channel *string `json:"channel" form:"channel" example:"test" `
Title *string `json:"title" form:"title" example:"Hello World" `
Content *string `json:"content" form:"content" example:"This is a message" `
Priority *int `json:"priority" form:"priority" example:"1" enums:"0,1,2" `
UserMessageID *string `json:"msg_id" form:"msg_id" example:"db8b0e6a-a08c-4646" `
SendTimestamp *float64 `json:"timestamp" form:"timestamp" example:"1669824037" `
SenderName *string `json:"sender_name" form:"sender_name" example:"example-server" `
KeyToken *string `json:"key" form:"key" example:"P3TNH8mvv14fm" `
Channel *string `json:"channel" form:"channel" example:"test" `
Title *string `json:"title" form:"title" example:"Hello World" `
Content *string `json:"content" form:"content" example:"This is a message" `
Priority *int `json:"priority" form:"priority" example:"1" enums:"0,1,2" `
UserMessageID *string `json:"msg_id" form:"msg_id" example:"db8b0e6a-a08c-4646" `
SendTimestamp *float64 `json:"timestamp" form:"timestamp" example:"1669824037" `
SenderName *string `json:"sender_name" form:"sender_name" example:"example-server" `
}
type response struct {
@@ -88,7 +88,7 @@ func (h MessageHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse
// query has highest prio, then form, then json
data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q)
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)
okResp, errResp := h.app.SendMessage(g, ctx, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName)
if errResp != nil {
return *errResp
} else {
+17 -4
View File
@@ -1,18 +1,19 @@
package handler
import (
"errors"
"net/http"
"regexp"
"strings"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"blackforestbytes.com/simplecloudnotifier/website"
"errors"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/rext"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"net/http"
"regexp"
"strings"
)
type WebsiteHandler struct {
@@ -77,6 +78,18 @@ func (h WebsiteHandler) MessageSent(pctx ginext.PreContext) ginext.HTTPResponse
})
}
func (h WebsiteHandler) PrivacyPolicy(pctx ginext.PreContext) ginext.HTTPResponse {
ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return h.serveAsset(g, "privacy_policy.html", true)
})
}
func (h WebsiteHandler) FaviconIco(pctx ginext.PreContext) ginext.HTTPResponse {
ctx, g, errResp := pctx.Start()
if errResp != nil {
+9 -1
View File
@@ -1,11 +1,12 @@
package api
import (
"errors"
"blackforestbytes.com/simplecloudnotifier/api/handler"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"blackforestbytes.com/simplecloudnotifier/swagger"
"errors"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
@@ -98,6 +99,10 @@ func (r *Router) Init(e *ginext.GinWrapper) error {
frontend.GET("/message_sent.php").Handle(r.websiteHandler.MessageSent)
frontend.GET("/message_sent.html").Handle(r.websiteHandler.MessageSent)
frontend.GET("/privacy_policy").Handle(r.websiteHandler.PrivacyPolicy)
frontend.GET("/privacy_policy.php").Handle(r.websiteHandler.PrivacyPolicy)
frontend.GET("/privacy_policy.html").Handle(r.websiteHandler.PrivacyPolicy)
frontend.GET("/favicon.ico").Handle(r.websiteHandler.FaviconIco)
frontend.GET("/favicon.png").Handle(r.websiteHandler.FaviconPNG)
@@ -159,12 +164,14 @@ func (r *Router) Init(e *ginext.GinWrapper) error {
apiv2.GET("/messages").Handle(r.apiHandler.ListMessages)
apiv2.GET("/messages/:mid").Handle(r.apiHandler.GetMessage)
apiv2.DELETE("/messages/:mid").Handle(r.apiHandler.DeleteMessage)
apiv2.GET("/messages/:mid/deliveries").Handle(r.apiHandler.ListMessageDeliveries)
apiv2.GET("/sender-names").Handle(r.apiHandler.ListSenderNames)
apiv2.GET("/preview/users/:uid").Handle(r.apiHandler.GetUserPreview)
apiv2.GET("/preview/keys/:kid").Handle(r.apiHandler.GetUserKeyPreview)
apiv2.GET("/preview/channels/:cid").Handle(r.apiHandler.GetChannelPreview)
apiv2.GET("/preview/clients/:cid").Handle(r.apiHandler.GetClientPreview)
}
// ================ Send API (unversioned) ================
@@ -176,6 +183,7 @@ func (r *Router) Init(e *ginext.GinWrapper) error {
sendAPI.POST("/send.php").Handle(r.compatHandler.SendMessage)
sendAPI.POST("/external/v1/uptime-kuma").Handle(r.externalHandler.UptimeKuma)
sendAPI.POST("/external/v1/shoutrrr").Handle(r.externalHandler.Shoutrrr)
}
+39 -10
View File
@@ -4,21 +4,27 @@ import (
"encoding/base32"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type Mode string //@enum:type
const (
CTMStart = "START"
CTMNormal = "NORMAL"
CTMEnd = "END"
CTMStart = "START"
CTMNormal = "NORMAL"
CTMPaginated = "PAGINATED"
CTMEnd = "END"
)
type CursorToken struct {
Mode Mode
Timestamp int64
Timestamp *int64
Page *int
Id string
Direction string
FilterHash string
@@ -34,7 +40,8 @@ type cursorTokenSerialize struct {
func Start() CursorToken {
return CursorToken{
Mode: CTMStart,
Timestamp: 0,
Timestamp: langext.Ptr[int64](0),
Page: nil,
Id: "",
Direction: "",
FilterHash: "",
@@ -44,7 +51,8 @@ func Start() CursorToken {
func End() CursorToken {
return CursorToken{
Mode: CTMEnd,
Timestamp: 0,
Timestamp: nil,
Page: nil,
Id: "",
Direction: "",
FilterHash: "",
@@ -54,13 +62,22 @@ func End() CursorToken {
func Normal(ts time.Time, id string, dir string, filter string) CursorToken {
return CursorToken{
Mode: CTMNormal,
Timestamp: ts.UnixMilli(),
Timestamp: langext.Ptr(ts.UnixMilli()),
Page: nil,
Id: id,
Direction: dir,
FilterHash: filter,
}
}
func Paginated(p int) CursorToken {
return CursorToken{
Mode: CTMPaginated,
Timestamp: nil,
Page: langext.Ptr(p),
}
}
func (c *CursorToken) Token() string {
if c.Mode == CTMStart {
return "@start"
@@ -69,6 +86,10 @@ func (c *CursorToken) Token() string {
return "@end"
}
if c.Page != nil {
return fmt.Sprintf("$%d", *c.Page)
}
// We kinda manually implement omitempty for the CursorToken here
// because omitempty does not work for time.Time and otherwise we would always
// get weird time values when decoding a token that initially didn't have an Timestamp set
@@ -80,8 +101,8 @@ func (c *CursorToken) Token() string {
sertok.Id = &c.Id
}
if c.Timestamp != 0 {
sertok.Timestamp = &c.Timestamp
if c.Timestamp != nil && *c.Timestamp != 0 {
sertok.Timestamp = langext.Ptr(*c.Timestamp)
}
if c.Direction != "" {
@@ -111,6 +132,14 @@ func Decode(tok string) (CursorToken, error) {
return End(), nil
}
if strings.HasPrefix(tok, "$") {
p, err := strconv.ParseInt(tok[1:], 10, 0)
if err != nil {
return CursorToken{}, errors.New("could not decode paginated token")
}
return Paginated(int(p)), nil
}
if !strings.HasPrefix(tok, "tok_") {
return CursorToken{}, errors.New("could not decode token, missing prefix")
}
@@ -129,7 +158,7 @@ func Decode(tok string) (CursorToken, error) {
token := CursorToken{Mode: CTMNormal}
if tokenDeserialize.Timestamp != nil {
token.Timestamp = *tokenDeserialize.Timestamp
token.Timestamp = langext.Ptr(*tokenDeserialize.Timestamp)
}
if tokenDeserialize.Id != nil {
token.Id = *tokenDeserialize.Id
+3 -2
View File
@@ -1,10 +1,11 @@
package primary
import (
"time"
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models"
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
"time"
)
func (db *Database) GetChannelByName(ctx db.TxContext, userid models.UserID, chanName string) (*models.Channel, error) {
@@ -16,7 +17,7 @@ func (db *Database) GetChannelByName(ctx db.TxContext, userid models.UserID, cha
return sq.QuerySingleOpt[models.Channel](ctx, tx, "SELECT * FROM channels WHERE owner_user_id = :uid AND internal_name = :nam AND deleted=0 LIMIT 1", sq.PP{"uid": userid, "nam": chanName}, sq.SModeExtended, sq.Safe)
}
func (db *Database) GetChannelByID(ctx db.TxContext, chanid models.ChannelID) (*models.Channel, error) {
func (db *Database) GetChannelByIDOpt(ctx db.TxContext, chanid models.ChannelID) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
+9
View File
@@ -53,6 +53,15 @@ func (db *Database) GetClient(ctx db.TxContext, userid models.UserID, clientid m
}, sq.SModeExtended, sq.Safe)
}
func (db *Database) GetClientByIDOpt(ctx db.TxContext, clientid models.ClientID) (*models.Client, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
return sq.QuerySingleOpt[models.Client](ctx, tx, "SELECT * FROM clients WHERE deleted=0 AND client_id = :cid LIMIT 1", sq.PP{"cid": clientid}, sq.SModeExtended, sq.Safe)
}
func (db *Database) GetClientOpt(ctx db.TxContext, userid models.UserID, clientid models.ClientID) (*models.Client, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
+11 -1
View File
@@ -1,12 +1,13 @@
package primary
import (
"time"
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
"time"
)
func (db *Database) CreateRetryDelivery(ctx db.TxContext, client models.Client, msg models.Message) (models.Delivery, error) {
@@ -182,3 +183,12 @@ func (db *Database) DeleteDeliveriesOfChannel(ctx db.TxContext, channelid models
return nil
}
func (db *Database) ListDeliveriesOfMessage(ctx db.TxContext, messageID models.MessageID) ([]models.Delivery, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
return sq.QueryAll[models.Delivery](ctx, tx, "SELECT * FROM deliveries WHERE message_id = :mid AND deleted=0 ORDER BY timestamp_created ASC", sq.PP{"mid": messageID}, sq.SModeExtended, sq.Safe)
}
+4 -3
View File
@@ -1,12 +1,13 @@
package primary
import (
"strings"
"time"
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
"strings"
"time"
)
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) {
@@ -67,7 +68,7 @@ func (db *Database) GetKeyTokenByID(ctx db.TxContext, keyTokenid models.KeyToken
return sq.QuerySingle[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE keytoken_id = :cid AND deleted=0 LIMIT 1", sq.PP{"cid": keyTokenid}, sq.SModeExtended, sq.Safe)
}
func (db *Database) GetKeyTokenByToken(ctx db.TxContext, key string) (*models.KeyToken, error) {
func (db *Database) GetKeyTokenByTokenOpt(ctx db.TxContext, key string) (*models.KeyToken, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
+28 -16
View File
@@ -1,12 +1,13 @@
package primary
import (
"errors"
"time"
"blackforestbytes.com/simplecloudnotifier/db"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models"
"errors"
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
"time"
)
func (db *Database) GetMessageByUserMessageID(ctx db.TxContext, usrMsgId string) (*models.Message, error) {
@@ -87,26 +88,32 @@ func (db *Database) ListMessages(ctx db.TxContext, filter models.MessageFilter,
return nil, ct.CursorToken{}, 0, err
}
pageCond := "1=1"
if inTok.Mode == ct.CTMNormal {
pageCond = "timestamp_real < :tokts OR (timestamp_real = :tokts AND message_id < :tokid )"
}
filterCond, filterJoin, prepParams, err := filter.SQL()
orderClause := ""
pageCond := "1=1"
limitCond := ""
if pageSize != nil {
orderClause = "ORDER BY COALESCE(timestamp_client, timestamp_real) DESC, message_id DESC LIMIT :lim"
limitCond = "LIMIT :lim"
prepParams["lim"] = *pageSize + 1
} else {
orderClause = "ORDER BY COALESCE(timestamp_client, timestamp_real) DESC, message_id DESC"
}
sqlQueryList := "SELECT " + "messages.*" + " FROM messages " + filterJoin + " WHERE ( " + pageCond + " ) AND ( " + filterCond + " ) " + orderClause
sqlQueryCount := "SELECT " + " COUNT(*) AS count FROM messages " + filterJoin + " WHERE ( " + filterCond + " ) "
if inTok.Mode == ct.CTMNormal {
pageCond = "timestamp_real < :tokts OR (timestamp_real = :tokts AND message_id < :tokid )"
prepParams["tokts"] = inTok.Timestamp
prepParams["tokid"] = inTok.Id
} else if inTok.Mode == ct.CTMPaginated {
if pageSize != nil {
limitCond = "LIMIT :lim OFFSET :off"
prepParams["lim"] = *pageSize + 1
prepParams["off"] = (*pageSize) * (*inTok.Page)
}
}
prepParams["tokts"] = inTok.Timestamp
prepParams["tokid"] = inTok.Id
orderClause := "ORDER BY COALESCE(timestamp_client, timestamp_real) DESC, message_id DESC"
sqlQueryList := "SELECT " + "messages.*" + " FROM messages " + filterJoin + " WHERE ( " + pageCond + " ) AND ( " + filterCond + " ) " + orderClause + " " + limitCond
sqlQueryCount := "SELECT " + " COUNT(*) AS count FROM messages " + filterJoin + " WHERE ( " + filterCond + " ) "
if inTok.Mode == ct.CTMEnd {
@@ -132,7 +139,12 @@ func (db *Database) ListMessages(ctx db.TxContext, filter models.MessageFilter,
return nil, ct.CursorToken{}, 0, err
}
outToken := ct.Normal(dataList[*pageSize-1].Timestamp(), dataList[*pageSize-1].MessageID.String(), "DESC", filter.Hash())
var outToken ct.CursorToken
if inTok.Mode == ct.CTMPaginated {
outToken = ct.Paginated(*inTok.Page + 1)
} else {
outToken = ct.Normal(dataList[*pageSize-1].Timestamp(), dataList[*pageSize-1].MessageID.String(), "DESC", filter.Hash())
}
return dataList[0:*pageSize], outToken, dataCount.Count, nil
}
+11 -1
View File
@@ -1,12 +1,13 @@
package primary
import (
"time"
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
"time"
)
func (db *Database) CreateUser(ctx db.TxContext, protoken *string, username *string) (models.User, error) {
@@ -63,6 +64,15 @@ func (db *Database) GetUser(ctx db.TxContext, userid models.UserID) (models.User
return sq.QuerySingle[models.User](ctx, tx, "SELECT * FROM users WHERE user_id = :uid AND deleted=0 LIMIT 1", sq.PP{"uid": userid}, sq.SModeExtended, sq.Safe)
}
func (db *Database) GetUserByKey(ctx db.TxContext, key string) (models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.User{}, err
}
return sq.QuerySingle[models.User](ctx, tx, "SELECT * FROM users WHERE EXISTS(SELECT keytokens.keytoken_id FROM keytokens WHERE keytokens.token = :tok AND users.user_id = keytokens.owner_user_id AND keytokens.deleted=0) AND users.deleted=0 LIMIT 1", sq.PP{"tok": key}, sq.SModeExtended, sq.Safe)
}
func (db *Database) GetUserOpt(ctx db.TxContext, userid models.UserID) (*models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
+22 -7
View File
@@ -52,14 +52,29 @@ func (db *Database) ListRequestLogs(ctx context.Context, filter models.RequestLo
return make([]models.RequestLog, 0), ct.End(), nil
}
pageCond := "1=1"
if inTok.Mode == ct.CTMNormal {
pageCond = "timestamp_created < :tokts OR (timestamp_created = :tokts AND request_id < :tokid )"
}
filterCond, filterJoin, prepParams, err := filter.SQL()
orderClause := ""
pageCond := "1=1"
limitCond := ""
if pageSize != nil {
limitCond = "LIMIT :lim"
prepParams["lim"] = *pageSize + 1
}
if inTok.Mode == ct.CTMNormal {
pageCond = "timestamp_created < :tokts OR (timestamp_created = :tokts AND request_id < :tokid )"
prepParams["tokts"] = inTok.Timestamp
prepParams["tokid"] = inTok.Id
} else if inTok.Mode == ct.CTMPaginated {
if pageSize != nil {
limitCond = "LIMIT :lim OFFSET :off"
prepParams["lim"] = *pageSize + 1
prepParams["off"] = (*pageSize) * (*inTok.Page)
}
}
orderClause := "ORDER BY timestamp_created DESC, request_id DESC"
if pageSize != nil {
orderClause = "ORDER BY timestamp_created DESC, request_id DESC LIMIT :lim"
prepParams["lim"] = *pageSize + 1
@@ -67,7 +82,7 @@ func (db *Database) ListRequestLogs(ctx context.Context, filter models.RequestLo
orderClause = "ORDER BY timestamp_created DESC, request_id DESC"
}
sqlQuery := "SELECT " + "requests.*" + " FROM requests " + filterJoin + " WHERE ( " + pageCond + " ) AND ( " + filterCond + " ) " + orderClause
sqlQuery := "SELECT " + "requests.*" + " FROM requests " + filterJoin + " WHERE ( " + pageCond + " ) AND ( " + filterCond + " ) " + orderClause + " " + limitCond
prepParams["tokts"] = inTok.Timestamp
prepParams["tokid"] = inTok.Id
+25 -22
View File
@@ -1,14 +1,14 @@
module blackforestbytes.com/simplecloudnotifier
go 1.23.0
go 1.24.0
toolchain go1.24.2
require (
git.blackforestbytes.com/BlackForestBytes/goext v0.0.575
github.com/gin-gonic/gin v1.10.0
git.blackforestbytes.com/BlackForestBytes/goext v0.0.614
github.com/gin-gonic/gin v1.11.0
github.com/glebarez/go-sqlite v1.22.0
github.com/go-playground/validator/v10 v10.26.0
github.com/go-playground/validator/v10 v10.28.0
github.com/go-sql-driver/mysql v1.8.1
github.com/jmoiron/sqlx v1.4.0
github.com/rs/zerolog v1.34.0
@@ -18,20 +18,22 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -39,24 +41,25 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.17.3 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
+58 -51
View File
@@ -1,27 +1,27 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.blackforestbytes.com/BlackForestBytes/goext v0.0.575 h1:scgvSaNZQ+C0pMbfPbsxF/hE3+rgLotHe+6Lbl5+mJU=
git.blackforestbytes.com/BlackForestBytes/goext v0.0.575/go.mod h1:Rj+bq1jLkgvXYe2sthg5UtXHf22nFvmTLeo+54fbYq8=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
git.blackforestbytes.com/BlackForestBytes/goext v0.0.614 h1:LMjy2iHEPHRfLCXhguG9mQ+cgRypy0o2OAKztzZXmqk=
git.blackforestbytes.com/BlackForestBytes/goext v0.0.614/go.mod h1:LTkvxOvjXqkfxjxMOCD+qUlQ6Cu5uPRXQ0rZodFl7Rw=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -30,17 +30,19 @@ 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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
@@ -50,12 +52,10 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@@ -81,6 +81,10 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8
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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
@@ -90,48 +94,51 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6
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=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/viney-shih/go-lock v1.1.2 h1:3TdGTiHZCPqBdTvFbQZQN/TRZzKF3KWw2rFEyKz3YqA=
github.com/viney-shih/go-lock v1.1.2/go.mod h1:Yijm78Ljteb3kRiJrbLAxVntkUukGu5uzSxq/xV7OO8=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -140,25 +147,26 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/loremipsum.v1 v1.1.2 h1:12APklfJKuGszqZsrArW5QoQh03/W+qyCCjvnDuS6Tw=
gopkg.in/loremipsum.v1 v1.1.2/go.mod h1:TuRvzFuzuejXj+odBU6Tubp/EPUyGb9wmSvHenyP2Ts=
@@ -174,4 +182,3 @@ modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+5 -4
View File
@@ -1,14 +1,15 @@
package jobs
import (
"errors"
"fmt"
"time"
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"github.com/rs/zerolog/log"
"time"
)
type DeliveryRetryJob struct {
@@ -208,7 +209,7 @@ func (j *DeliveryRetryJob) redeliver(ctx *simplectx.SimpleContext, delivery mode
return
}
channel, err := j.app.Database.Primary.GetChannelByID(ctx, msg.ChannelID)
channel, err := j.app.Database.Primary.GetChannelByIDOpt(ctx, msg.ChannelID)
if err != nil {
log.Err(err).Str("ChannelID", msg.ChannelID.String()).Msg("Failed to get channel")
ctx.RollbackTransaction()
+1 -1
View File
@@ -245,7 +245,7 @@ func (app *Application) getPermissions(ctx db.TxContext, hdr string) (models.Per
key := strings.TrimSpace(hdr[4:])
tok, err := app.Database.Primary.GetKeyTokenByToken(ctx, key)
tok, err := app.Database.Primary.GetKeyTokenByTokenOpt(ctx, key)
if err != nil {
return models.PermissionSet{}, err
}
+12 -14
View File
@@ -1,21 +1,22 @@
package logic
import (
"database/sql"
"errors"
"fmt"
"strings"
"time"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/mathext"
"git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"strings"
"time"
)
type SendMessageResponse struct {
@@ -25,7 +26,7 @@ type SendMessageResponse struct {
CompatMessageID int64
}
func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *models.UserID, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginext.HTTPResponse) {
func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginext.HTTPResponse) {
if Title != nil {
Title = langext.Ptr(strings.TrimSpace(*Title))
}
@@ -33,9 +34,6 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
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))
}
@@ -49,9 +47,9 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.NO_TITLE, hl.TITLE, "No title specified", nil))
}
user, err := app.Database.Primary.GetUser(ctx, *UserID)
user, err := app.Database.Primary.GetUserByKey(ctx, *Key)
if errors.Is(err, sql.ErrNoRows) {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", err))
return nil, langext.Ptr(ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, hl.USER_KEY, "Key not found or not valid", err))
}
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err))
@@ -126,7 +124,7 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
return nil, langext.Ptr(ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, hl.NONE, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil))
}
channel, err := app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName)
channel, err := app.GetOrCreateChannel(ctx, user.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))
}
@@ -145,7 +143,7 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
clientIP := g.ClientIP()
msg, err := app.Database.Primary.CreateMessage(ctx, *UserID, channel, sendTimestamp, *Title, Content, priority, UserMessageID, clientIP, SenderName, keytok.KeyTokenID)
msg, err := app.Database.Primary.CreateMessage(ctx, user.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))
}
@@ -176,7 +174,7 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
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 (to %d active subscriptions)", msg.MessageID, UserID, len(activeSubscriptions)))
log.Info().Msg(fmt.Sprintf("Sending new notification %s for user %s (to %d active subscriptions)", msg.MessageID, user.UserID, len(activeSubscriptions)))
for _, sub := range activeSubscriptions {
clients, err := app.Database.Primary.ListClients(ctx, sub.SubscriberUserID)
+1 -1
View File
@@ -75,7 +75,7 @@ func (ac *AppContext) CheckPermissionUserAdmin(userid models.UserID) *ginext.HTT
func (ac *AppContext) CheckPermissionSend(channel models.Channel, key string) (*models.KeyToken, *ginext.HTTPResponse) {
keytok, err := ac.app.Database.Primary.GetKeyTokenByToken(ac, key)
keytok, err := ac.app.Database.Primary.GetKeyTokenByTokenOpt(ac, key)
if err != nil {
return nil, langext.Ptr(ginresp.APIError(ac.ginContext, 500, apierr.DATABASE_ERROR, "Failed to query token", err))
}
+22
View File
@@ -21,3 +21,25 @@ type Client struct {
Name *string `db:"name" json:"name"`
Deleted bool `db:"deleted" json:"-"`
}
type ClientPreview struct {
ClientID ClientID `json:"client_id"`
UserID UserID `json:"user_id"`
Type ClientType `json:"type"`
TimestampCreated SCNTime `json:"timestamp_created"`
AgentModel string `json:"agent_model"`
AgentVersion string `json:"agent_version"`
Name *string `json:"name"`
}
func (c Client) Preview() ClientPreview {
return ClientPreview{
ClientID: c.ClientID,
UserID: c.UserID,
Type: c.Type,
TimestampCreated: c.TimestampCreated,
AgentModel: c.AgentModel,
AgentVersion: c.AgentVersion,
Name: c.Name,
}
}
-375
View File
@@ -1,375 +0,0 @@
// Code generated by enum-generate.go DO NOT EDIT.
package models
import "git.blackforestbytes.com/BlackForestBytes/goext/langext"
import "git.blackforestbytes.com/BlackForestBytes/goext/enums"
const ChecksumEnumGenerator = "1e8100b30bf6c946a1dfdc273b41efcaa91f33eab2bda12ce5dfa853741ac90b" // GoExtVersion: 0.0.575
// ================================ ClientType ================================
//
// File: client.go
// StringEnum: true
// DescrEnum: false
// DataEnum: false
//
var __ClientTypeValues = []ClientType{
ClientTypeAndroid,
ClientTypeIOS,
ClientTypeLinux,
ClientTypeMacOS,
ClientTypeWindows,
}
var __ClientTypeVarnames = map[ClientType]string{
ClientTypeAndroid: "ClientTypeAndroid",
ClientTypeIOS: "ClientTypeIOS",
ClientTypeLinux: "ClientTypeLinux",
ClientTypeMacOS: "ClientTypeMacOS",
ClientTypeWindows: "ClientTypeWindows",
}
func (e ClientType) Valid() bool {
return langext.InArray(e, __ClientTypeValues)
}
func (e ClientType) Values() []ClientType {
return __ClientTypeValues
}
func (e ClientType) ValuesAny() []any {
return langext.ArrCastToAny(__ClientTypeValues)
}
func (e ClientType) ValuesMeta() []enums.EnumMetaValue {
return ClientTypeValuesMeta()
}
func (e ClientType) String() string {
return string(e)
}
func (e ClientType) VarName() string {
if d, ok := __ClientTypeVarnames[e]; ok {
return d
}
return ""
}
func (e ClientType) TypeName() string {
return "ClientType"
}
func (e ClientType) PackageName() string {
return "models"
}
func (e ClientType) Meta() enums.EnumMetaValue {
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
}
func ParseClientType(vv string) (ClientType, bool) {
for _, ev := range __ClientTypeValues {
if string(ev) == vv {
return ev, true
}
}
return "", false
}
func ClientTypeValues() []ClientType {
return __ClientTypeValues
}
func ClientTypeValuesMeta() []enums.EnumMetaValue {
return []enums.EnumMetaValue{
ClientTypeAndroid.Meta(),
ClientTypeIOS.Meta(),
ClientTypeLinux.Meta(),
ClientTypeMacOS.Meta(),
ClientTypeWindows.Meta(),
}
}
// ================================ DeliveryStatus ================================
//
// File: delivery.go
// StringEnum: true
// DescrEnum: false
// DataEnum: false
//
var __DeliveryStatusValues = []DeliveryStatus{
DeliveryStatusRetry,
DeliveryStatusSuccess,
DeliveryStatusFailed,
}
var __DeliveryStatusVarnames = map[DeliveryStatus]string{
DeliveryStatusRetry: "DeliveryStatusRetry",
DeliveryStatusSuccess: "DeliveryStatusSuccess",
DeliveryStatusFailed: "DeliveryStatusFailed",
}
func (e DeliveryStatus) Valid() bool {
return langext.InArray(e, __DeliveryStatusValues)
}
func (e DeliveryStatus) Values() []DeliveryStatus {
return __DeliveryStatusValues
}
func (e DeliveryStatus) ValuesAny() []any {
return langext.ArrCastToAny(__DeliveryStatusValues)
}
func (e DeliveryStatus) ValuesMeta() []enums.EnumMetaValue {
return DeliveryStatusValuesMeta()
}
func (e DeliveryStatus) String() string {
return string(e)
}
func (e DeliveryStatus) VarName() string {
if d, ok := __DeliveryStatusVarnames[e]; ok {
return d
}
return ""
}
func (e DeliveryStatus) TypeName() string {
return "DeliveryStatus"
}
func (e DeliveryStatus) PackageName() string {
return "models"
}
func (e DeliveryStatus) Meta() enums.EnumMetaValue {
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
}
func ParseDeliveryStatus(vv string) (DeliveryStatus, bool) {
for _, ev := range __DeliveryStatusValues {
if string(ev) == vv {
return ev, true
}
}
return "", false
}
func DeliveryStatusValues() []DeliveryStatus {
return __DeliveryStatusValues
}
func DeliveryStatusValuesMeta() []enums.EnumMetaValue {
return []enums.EnumMetaValue{
DeliveryStatusRetry.Meta(),
DeliveryStatusSuccess.Meta(),
DeliveryStatusFailed.Meta(),
}
}
// ================================ TokenPerm ================================
//
// File: keytoken.go
// StringEnum: true
// DescrEnum: true
// DataEnum: false
//
var __TokenPermValues = []TokenPerm{
PermAdmin,
PermChannelRead,
PermChannelSend,
PermUserRead,
}
var __TokenPermDescriptions = map[TokenPerm]string{
PermAdmin: "Edit userdata (+ includes all other permissions)",
PermChannelRead: "Read messages",
PermChannelSend: "Send messages",
PermUserRead: "Read userdata",
}
var __TokenPermVarnames = map[TokenPerm]string{
PermAdmin: "PermAdmin",
PermChannelRead: "PermChannelRead",
PermChannelSend: "PermChannelSend",
PermUserRead: "PermUserRead",
}
func (e TokenPerm) Valid() bool {
return langext.InArray(e, __TokenPermValues)
}
func (e TokenPerm) Values() []TokenPerm {
return __TokenPermValues
}
func (e TokenPerm) ValuesAny() []any {
return langext.ArrCastToAny(__TokenPermValues)
}
func (e TokenPerm) ValuesMeta() []enums.EnumMetaValue {
return TokenPermValuesMeta()
}
func (e TokenPerm) String() string {
return string(e)
}
func (e TokenPerm) Description() string {
if d, ok := __TokenPermDescriptions[e]; ok {
return d
}
return ""
}
func (e TokenPerm) VarName() string {
if d, ok := __TokenPermVarnames[e]; ok {
return d
}
return ""
}
func (e TokenPerm) TypeName() string {
return "TokenPerm"
}
func (e TokenPerm) PackageName() string {
return "models"
}
func (e TokenPerm) Meta() enums.EnumMetaValue {
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: langext.Ptr(e.Description())}
}
func (e TokenPerm) DescriptionMeta() enums.EnumDescriptionMetaValue {
return enums.EnumDescriptionMetaValue{VarName: e.VarName(), Value: e, Description: e.Description()}
}
func ParseTokenPerm(vv string) (TokenPerm, bool) {
for _, ev := range __TokenPermValues {
if string(ev) == vv {
return ev, true
}
}
return "", false
}
func TokenPermValues() []TokenPerm {
return __TokenPermValues
}
func TokenPermValuesMeta() []enums.EnumMetaValue {
return []enums.EnumMetaValue{
PermAdmin.Meta(),
PermChannelRead.Meta(),
PermChannelSend.Meta(),
PermUserRead.Meta(),
}
}
func TokenPermValuesDescriptionMeta() []enums.EnumDescriptionMetaValue {
return []enums.EnumDescriptionMetaValue{
PermAdmin.DescriptionMeta(),
PermChannelRead.DescriptionMeta(),
PermChannelSend.DescriptionMeta(),
PermUserRead.DescriptionMeta(),
}
}
// ================================ TransactionLockMode ================================
//
// File: lock.go
// StringEnum: true
// DescrEnum: false
// DataEnum: false
//
var __TransactionLockModeValues = []TransactionLockMode{
TLockNone,
TLockRead,
TLockReadWrite,
}
var __TransactionLockModeVarnames = map[TransactionLockMode]string{
TLockNone: "TLockNone",
TLockRead: "TLockRead",
TLockReadWrite: "TLockReadWrite",
}
func (e TransactionLockMode) Valid() bool {
return langext.InArray(e, __TransactionLockModeValues)
}
func (e TransactionLockMode) Values() []TransactionLockMode {
return __TransactionLockModeValues
}
func (e TransactionLockMode) ValuesAny() []any {
return langext.ArrCastToAny(__TransactionLockModeValues)
}
func (e TransactionLockMode) ValuesMeta() []enums.EnumMetaValue {
return TransactionLockModeValuesMeta()
}
func (e TransactionLockMode) String() string {
return string(e)
}
func (e TransactionLockMode) VarName() string {
if d, ok := __TransactionLockModeVarnames[e]; ok {
return d
}
return ""
}
func (e TransactionLockMode) TypeName() string {
return "TransactionLockMode"
}
func (e TransactionLockMode) PackageName() string {
return "models"
}
func (e TransactionLockMode) Meta() enums.EnumMetaValue {
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
}
func ParseTransactionLockMode(vv string) (TransactionLockMode, bool) {
for _, ev := range __TransactionLockModeValues {
if string(ev) == vv {
return ev, true
}
}
return "", false
}
func TransactionLockModeValues() []TransactionLockMode {
return __TransactionLockModeValues
}
func TransactionLockModeValuesMeta() []enums.EnumMetaValue {
return []enums.EnumMetaValue{
TLockNone.Meta(),
TLockRead.Meta(),
TLockReadWrite.Meta(),
}
}
// ================================ ================= ================================
func AllPackageEnums() []enums.Enum {
return []enums.Enum{
ClientTypeAndroid, // ClientType
DeliveryStatusRetry, // DeliveryStatus
PermAdmin, // TokenPerm
TLockNone, // TransactionLockMode
}
}
-410
View File
@@ -1,410 +0,0 @@
// Code generated by csid-generate.go DO NOT EDIT.
package models
import "crypto/rand"
import "crypto/sha256"
import "fmt"
import "github.com/go-playground/validator/v10"
import "github.com/rs/zerolog/log"
import "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
import "git.blackforestbytes.com/BlackForestBytes/goext/langext"
import "git.blackforestbytes.com/BlackForestBytes/goext/rext"
import "math/big"
import "reflect"
import "regexp"
import "strings"
const ChecksumCharsetIDGenerator = "1e8100b30bf6c946a1dfdc273b41efcaa91f33eab2bda12ce5dfa853741ac90b" // GoExtVersion: 0.0.575
const idlen = 24
const checklen = 1
const idCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const idCharsetLen = len(idCharset)
var charSetReverseMap = generateCharsetMap()
const (
prefixUserID = "USR"
prefixChannelID = "CHA"
prefixDeliveryID = "DEL"
prefixMessageID = "MSG"
prefixSubscriptionID = "SUB"
prefixClientID = "CLN"
prefixRequestID = "REQ"
prefixKeyTokenID = "TOK"
)
var (
regexUserID = generateRegex(prefixUserID)
regexChannelID = generateRegex(prefixChannelID)
regexDeliveryID = generateRegex(prefixDeliveryID)
regexMessageID = generateRegex(prefixMessageID)
regexSubscriptionID = generateRegex(prefixSubscriptionID)
regexClientID = generateRegex(prefixClientID)
regexRequestID = generateRegex(prefixRequestID)
regexKeyTokenID = generateRegex(prefixKeyTokenID)
)
func generateRegex(prefix string) rext.Regex {
return rext.W(regexp.MustCompile(fmt.Sprintf("^%s[%s]{%d}[%s]{%d}$", prefix, idCharset, idlen-len(prefix)-checklen, idCharset, checklen)))
}
func generateCharsetMap() []int {
result := make([]int, 128)
for i := 0; i < len(result); i++ {
result[i] = -1
}
for idx, chr := range idCharset {
result[int(chr)] = idx
}
return result
}
func generateID(prefix string) string {
k := ""
csMax := big.NewInt(int64(idCharsetLen))
checksum := 0
for i := 0; i < idlen-len(prefix)-checklen; i++ {
v, err := rand.Int(rand.Reader, csMax)
if err != nil {
panic(err)
}
v64 := v.Int64()
k += string(idCharset[v64])
checksum = (checksum + int(v64)) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
return prefix + k + checkstr
}
func generateIDFromSeed(prefix string, seed string) string {
h := sha256.New()
iddata := ""
for len(iddata) < idlen-len(prefix)-checklen {
h.Write([]byte(seed))
bs := h.Sum(nil)
iddata += langext.NewAnyBaseConverter(idCharset).Encode(bs)
}
checksum := 0
for i := 0; i < idlen-len(prefix)-checklen; i++ {
ichr := int(iddata[i])
checksum = (checksum + charSetReverseMap[ichr]) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
return prefix + iddata[:(idlen-len(prefix)-checklen)] + checkstr
}
func validateID(prefix string, value string) error {
if len(value) != idlen {
return exerr.New(exerr.TypeInvalidCSID, "id has the wrong length").Str("value", value).Build()
}
if !strings.HasPrefix(value, prefix) {
return exerr.New(exerr.TypeInvalidCSID, "id is missing the correct prefix").Str("value", value).Str("prefix", prefix).Build()
}
checksum := 0
for i := len(prefix); i < len(value)-checklen; i++ {
ichr := int(value[i])
if ichr < 0 || ichr >= len(charSetReverseMap) || charSetReverseMap[ichr] == -1 {
return exerr.New(exerr.TypeInvalidCSID, "id contains invalid characters").Str("value", value).Build()
}
checksum = (checksum + charSetReverseMap[ichr]) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
if !strings.HasSuffix(value, checkstr) {
return exerr.New(exerr.TypeInvalidCSID, "id checkstring is invalid").Str("value", value).Str("checkstr", checkstr).Build()
}
return nil
}
func getRawData(prefix string, value string) string {
if len(value) != idlen {
return ""
}
return value[len(prefix) : idlen-checklen]
}
func getCheckString(prefix string, value string) string {
if len(value) != idlen {
return ""
}
return value[idlen-checklen:]
}
func ValidateEntityID(vfl validator.FieldLevel) bool {
if !vfl.Field().CanInterface() {
log.Error().Msgf("Failed to validate EntityID (cannot interface ?!?)")
return false
}
ifvalue := vfl.Field().Interface()
if value1, ok := ifvalue.(EntityID); ok {
if vfl.Field().Type().Kind() == reflect.Pointer && langext.IsNil(value1) {
return true
}
if err := value1.Valid(); err != nil {
log.Debug().Msgf("Failed to validate EntityID '%s' (%s)", value1.String(), err.Error())
return false
} else {
return true
}
} else {
log.Error().Msgf("Failed to validate EntityID (wrong type: %T)", ifvalue)
return false
}
}
// ================================ UserID (ids.go) ================================
func NewUserID() UserID {
return UserID(generateID(prefixUserID))
}
func (id UserID) Valid() error {
return validateID(prefixUserID, string(id))
}
func (i UserID) String() string {
return string(i)
}
func (i UserID) Prefix() string {
return prefixUserID
}
func (id UserID) Raw() string {
return getRawData(prefixUserID, string(id))
}
func (id UserID) CheckString() string {
return getCheckString(prefixUserID, string(id))
}
func (id UserID) Regex() rext.Regex {
return regexUserID
}
// ================================ ChannelID (ids.go) ================================
func NewChannelID() ChannelID {
return ChannelID(generateID(prefixChannelID))
}
func (id ChannelID) Valid() error {
return validateID(prefixChannelID, string(id))
}
func (i ChannelID) String() string {
return string(i)
}
func (i ChannelID) Prefix() string {
return prefixChannelID
}
func (id ChannelID) Raw() string {
return getRawData(prefixChannelID, string(id))
}
func (id ChannelID) CheckString() string {
return getCheckString(prefixChannelID, string(id))
}
func (id ChannelID) Regex() rext.Regex {
return regexChannelID
}
// ================================ DeliveryID (ids.go) ================================
func NewDeliveryID() DeliveryID {
return DeliveryID(generateID(prefixDeliveryID))
}
func (id DeliveryID) Valid() error {
return validateID(prefixDeliveryID, string(id))
}
func (i DeliveryID) String() string {
return string(i)
}
func (i DeliveryID) Prefix() string {
return prefixDeliveryID
}
func (id DeliveryID) Raw() string {
return getRawData(prefixDeliveryID, string(id))
}
func (id DeliveryID) CheckString() string {
return getCheckString(prefixDeliveryID, string(id))
}
func (id DeliveryID) Regex() rext.Regex {
return regexDeliveryID
}
// ================================ MessageID (ids.go) ================================
func NewMessageID() MessageID {
return MessageID(generateID(prefixMessageID))
}
func (id MessageID) Valid() error {
return validateID(prefixMessageID, string(id))
}
func (i MessageID) String() string {
return string(i)
}
func (i MessageID) Prefix() string {
return prefixMessageID
}
func (id MessageID) Raw() string {
return getRawData(prefixMessageID, string(id))
}
func (id MessageID) CheckString() string {
return getCheckString(prefixMessageID, string(id))
}
func (id MessageID) Regex() rext.Regex {
return regexMessageID
}
// ================================ SubscriptionID (ids.go) ================================
func NewSubscriptionID() SubscriptionID {
return SubscriptionID(generateID(prefixSubscriptionID))
}
func (id SubscriptionID) Valid() error {
return validateID(prefixSubscriptionID, string(id))
}
func (i SubscriptionID) String() string {
return string(i)
}
func (i SubscriptionID) Prefix() string {
return prefixSubscriptionID
}
func (id SubscriptionID) Raw() string {
return getRawData(prefixSubscriptionID, string(id))
}
func (id SubscriptionID) CheckString() string {
return getCheckString(prefixSubscriptionID, string(id))
}
func (id SubscriptionID) Regex() rext.Regex {
return regexSubscriptionID
}
// ================================ ClientID (ids.go) ================================
func NewClientID() ClientID {
return ClientID(generateID(prefixClientID))
}
func (id ClientID) Valid() error {
return validateID(prefixClientID, string(id))
}
func (i ClientID) String() string {
return string(i)
}
func (i ClientID) Prefix() string {
return prefixClientID
}
func (id ClientID) Raw() string {
return getRawData(prefixClientID, string(id))
}
func (id ClientID) CheckString() string {
return getCheckString(prefixClientID, string(id))
}
func (id ClientID) Regex() rext.Regex {
return regexClientID
}
// ================================ RequestID (ids.go) ================================
func NewRequestID() RequestID {
return RequestID(generateID(prefixRequestID))
}
func (id RequestID) Valid() error {
return validateID(prefixRequestID, string(id))
}
func (i RequestID) String() string {
return string(i)
}
func (i RequestID) Prefix() string {
return prefixRequestID
}
func (id RequestID) Raw() string {
return getRawData(prefixRequestID, string(id))
}
func (id RequestID) CheckString() string {
return getCheckString(prefixRequestID, string(id))
}
func (id RequestID) Regex() rext.Regex {
return regexRequestID
}
// ================================ KeyTokenID (ids.go) ================================
func NewKeyTokenID() KeyTokenID {
return KeyTokenID(generateID(prefixKeyTokenID))
}
func (id KeyTokenID) Valid() error {
return validateID(prefixKeyTokenID, string(id))
}
func (i KeyTokenID) String() string {
return string(i)
}
func (i KeyTokenID) Prefix() string {
return prefixKeyTokenID
}
func (id KeyTokenID) Raw() string {
return getRawData(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) CheckString() string {
return getCheckString(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) Regex() rext.Regex {
return regexKeyTokenID
}
+3 -3
View File
@@ -88,9 +88,9 @@ func (u User) MaxTitleLength() int {
func (u User) QuotaPerDay() int {
if u.IsPro {
return 5000
return 15_000
} else {
return 50
return 500
}
}
@@ -135,7 +135,7 @@ func (u User) MaxTimestampDiffHours() int {
return 24
}
func (u User) JSONPreview() UserPreview {
func (u User) Preview() UserPreview {
return UserPreview{
UserID: u.UserID,
Username: u.Username,
+13 -6
View File
@@ -1,22 +1,23 @@
package push
import (
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/models"
"bytes"
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"io"
"net/http"
"strconv"
"strings"
"time"
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/models"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
// https://firebase.google.com/docs/cloud-messaging/send-message#rest
@@ -66,7 +67,13 @@ func (fb FirebaseConnector) SendNotification(ctx context.Context, user models.Us
"title": msg.Title,
"body": msg.ShortContent(),
},
"apns": gin.H{},
"apns": gin.H{
"payload": gin.H{
"aps": gin.H{
"sound": "default",
},
},
},
}
} else if client.Type == models.ClientTypeAndroid {
jsonBody = gin.H{
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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