Merge branch 'flutter_app'
All checks were successful
Build Docker and Deploy / Build Docker Container (push) Successful in 1m59s
Build Docker and Deploy / Deploy to Server (push) Successful in 7s

This commit is contained in:
Mike Schwörer 2024-06-12 00:36:12 +02:00
commit dac268f40b
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
37 changed files with 1650 additions and 402 deletions

2
flutter/.gitignore vendored
View File

@ -2,6 +2,8 @@
*.keystore
firepit-log.txt
flutter_jank_*
#######################################################################################################################

View File

@ -7,6 +7,9 @@ run:
test:
dart analyze
fix:
dart fix --apply
gen:
dart run build_runner build

View File

@ -24,7 +24,7 @@
pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' )"
pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' | tail -n 1 )"
if [ -z "$pid" ]; then
red "No [flutter run] process found - exiting"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,6 @@ import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/request_log.dart';
@ -182,6 +181,16 @@ class APIClient {
);
}
static Future<ChannelWithSubscription> getChannel(TokenSource auth, String cid) async {
return await _request(
name: 'getChannel',
method: 'GET',
relURL: 'users/${auth.getUserID()}/channels/${cid}',
fn: ChannelWithSubscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<(String, List<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
return await _request(
name: 'getMessageList',

View File

@ -1,6 +1,6 @@
class APIException implements Exception {
final int httpStatus;
final String error;
final int error;
final String errHighlight;
final String message;

View File

@ -11,46 +11,64 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
required this.showThemeSwitch,
required this.showDebug,
required this.showSearch,
required this.showShare,
this.onShare = null,
}) : super(key: key);
final String? title;
final bool showThemeSwitch;
final bool showDebug;
final bool showSearch;
final bool showShare;
final void Function()? onShare;
@override
Widget build(BuildContext context) {
var actions = <Widget>[];
if (showThemeSwitch) {
actions.add(Consumer<AppTheme>(
builder: (context, appTheme, child) => IconButton(
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode',
onPressed: appTheme.switchDarkMode,
),
));
} else {
actions.add(SizedBox.square(dimension: 40));
}
if (showDebug) {
actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
tooltip: 'Debug',
onPressed: () {
Navigator.push(context, MaterialPageRoute<DebugMainPage>(builder: (context) => DebugMainPage()));
},
));
} else {
actions.add(SizedBox.square(dimension: 40));
}
if (showSearch) {
actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass),
tooltip: 'Search',
onPressed: () {/*TODO*/},
));
} else if (showShare) {
actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidShareNodes),
tooltip: 'Share',
onPressed: onShare ?? () {},
));
} else {
actions.add(SizedBox.square(dimension: 40));
}
return AppBar(
title: Text(title ?? 'Simple Cloud Notifier 2.0'),
actions: <Widget>[
if (showThemeSwitch)
Consumer<AppTheme>(
builder: (context, appTheme, child) => IconButton(
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
tooltip: 'Debug',
onPressed: () {
appTheme.switchDarkMode();
},
),
),
if (!showThemeSwitch) SizedBox.square(dimension: 40),
if (showDebug)
IconButton(
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
tooltip: 'Debug',
onPressed: () {
Navigator.push(context, MaterialPageRoute<DebugMainPage>(builder: (context) => DebugMainPage()));
},
),
if (!showDebug) SizedBox.square(dimension: 40),
if (showSearch)
IconButton(
icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass),
tooltip: 'Search',
onPressed: () {},
),
if (!showSearch) SizedBox.square(dimension: 40),
],
actions: actions,
backgroundColor: Theme.of(context).secondaryHeaderColor,
);
}

View File

@ -9,6 +9,8 @@ class SCNScaffold extends StatelessWidget {
this.showThemeSwitch = true,
this.showDebug = true,
this.showSearch = true,
this.showShare = false,
this.onShare = null,
}) : super(key: key);
final Widget child;
@ -16,6 +18,8 @@ class SCNScaffold extends StatelessWidget {
final bool showThemeSwitch;
final bool showDebug;
final bool showSearch;
final bool showShare;
final void Function()? onShare;
@override
Widget build(BuildContext context) {
@ -25,6 +29,8 @@ class SCNScaffold extends StatelessWidget {
showThemeSwitch: showThemeSwitch,
showDebug: showDebug,
showSearch: showSearch,
showShare: showShare,
onShare: onShare ?? () {},
),
body: child,
);

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -17,15 +19,24 @@ import 'firebase_options.dart';
void main() async {
print('[INIT] Application starting...');
print('[INIT] Ensure WidgetsFlutterBinding...');
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
print('[INIT] Init Globals...');
await Globals().init();
print('[INIT] Init Hive...');
await Hive.initFlutter();
Hive.registerAdapter(SCNRequestAdapter());
Hive.registerAdapter(SCNLogAdapter());
Hive.registerAdapter(SCNLogLevelAdapter());
print('[INIT] Load Hive<scn-requests>...');
try {
await Hive.openBox<SCNRequest>('scn-requests');
} catch (exc, trace) {
@ -34,6 +45,8 @@ void main() async {
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
}
print('[INIT] Load Hive<scn-logs>...');
try {
await Hive.openBox<SCNLog>('scn-logs');
} catch (exc, trace) {
@ -42,45 +55,58 @@ void main() async {
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
}
print('[INIT] Load AppAuth...');
final appAuth = AppAuth(); // ensure UserAccount is loaded
if (appAuth.isAuth()) {
try {
print('[INIT] Load User...');
await appAuth.loadUser();
//TODO fallback to cached user (perhaps event use cached client (if exists) directly and only update/load in background)
} catch (exc, trace) {
ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace);
}
try {
print('[INIT] Load Client...');
await appAuth.loadClient();
//TODO fallback to cached client (perhaps event use cached client (if exists) directly and only update/load in background)
} catch (exc, trace) {
ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace);
}
}
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
if (!Platform.isLinux) {
print('[INIT] Init Firebase...');
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseMessaging.instance.requestPermission(provisional: true);
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);
print('[INIT] Query firebase token...');
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken != null) {
setFirebaseToken(fcmToken);
}
} catch (exc, trace) {
ApplicationLog.error('Failed to set firebase token: ' + exc.toString(), trace: trace);
ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
}
}).onError((dynamic err) {
ApplicationLog.error('Failed to listen to token refresh events: ' + (err?.toString() ?? ''));
});
try {
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken != null) {
setFirebaseToken(fcmToken);
}
} catch (exc, trace) {
ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
} else {
print('[INIT] Skip Firebase init (Platform == Linux)...');
}
ApplicationLog.debug('Application started');
ApplicationLog.debug('[INIT] Application started');
runApp(
MultiProvider(
@ -112,7 +138,7 @@ void setFirebaseToken(String fcmToken) async {
return;
}
if (oldToken != null && oldToken == fcmToken && client != null && client!.fcmToken == fcmToken) {
if (oldToken != null && oldToken == fcmToken && client != null && client.fcmToken == fcmToken) {
ApplicationLog.info('Firebase token unchanged - do nothing', additional: 'Token: $fcmToken');
return;
}

View File

@ -1,9 +1,61 @@
class APIError {
final String success;
final String error;
final bool success;
final int error;
final String errhighlight;
final String message;
static final MISSING_UID = 1101;
static final MISSING_TOK = 1102;
static final MISSING_TITLE = 1103;
static final INVALID_PRIO = 1104;
static final REQ_METHOD = 1105;
static final INVALID_CLIENTTYPE = 1106;
static final PAGETOKEN_ERROR = 1121;
static final BINDFAIL_QUERY_PARAM = 1151;
static final BINDFAIL_BODY_PARAM = 1152;
static final BINDFAIL_URI_PARAM = 1153;
static final INVALID_BODY_PARAM = 1161;
static final INVALID_ENUM_VALUE = 1171;
static final NO_TITLE = 1201;
static final TITLE_TOO_LONG = 1202;
static final CONTENT_TOO_LONG = 1203;
static final USR_MSG_ID_TOO_LONG = 1204;
static final TIMESTAMP_OUT_OF_RANGE = 1205;
static final SENDERNAME_TOO_LONG = 1206;
static final CHANNEL_TOO_LONG = 1207;
static final CHANNEL_DESCRIPTION_TOO_LONG = 1208;
static final CHANNEL_NAME_EMPTY = 1209;
static final USER_NOT_FOUND = 1301;
static final CLIENT_NOT_FOUND = 1302;
static final CHANNEL_NOT_FOUND = 1303;
static final SUBSCRIPTION_NOT_FOUND = 1304;
static final MESSAGE_NOT_FOUND = 1305;
static final SUBSCRIPTION_USER_MISMATCH = 1306;
static final KEY_NOT_FOUND = 1307;
static final USER_AUTH_FAILED = 1311;
static final NO_DEVICE_LINKED = 1401;
static final CHANNEL_ALREADY_EXISTS = 1501;
static final CANNOT_SELFDELETE_KEY = 1511;
static final CANNOT_SELFUPDATE_KEY = 1512;
static final QUOTA_REACHED = 2101;
static final FAILED_VERIFY_PRO_TOKEN = 3001;
static final INVALID_PRO_TOKEN = 3002;
static final COMMIT_FAILED = 9001;
static final DATABASE_ERROR = 9002;
static final PERM_QUERY_FAIL = 9003;
static final FIREBASE_COM_FAILED = 9901;
static final FIREBASE_COM_ERRORED = 9902;
static final INTERNAL_EXCEPTION = 9903;
static final PANIC = 9904;
static final NOT_IMPLEMENTED = 9905;
const APIError({
required this.success,
required this.error,
@ -13,8 +65,8 @@ class APIError {
factory APIError.fromJson(Map<String, dynamic> json) {
return APIError(
success: json['success'] as String,
error: json['error'] as String,
success: json['success'] as bool,
error: (json['error'] as double).toInt(),
errhighlight: json['errhighlight'] as String,
message: json['message'] as String,
);

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_lazy_indexed_stack/flutter_lazy_indexed_stack.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/components/hidable_fab/hidable_fab.dart';
@ -62,15 +61,16 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
title: null,
showDebug: true,
showSearch: _selectedIndex == 0 || _selectedIndex == 1,
showShare: false,
showThemeSwitch: true,
),
body: LazyIndexedStack(
body: IndexedStack(
children: [
MessageListPage(),
ChannelRootPage(),
AccountRootPage(),
SettingsRootPage(),
SendRootPage(),
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage()),
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage()),
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage()),
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage()),
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage()),
],
index: _selectedIndex,
),

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -9,6 +11,8 @@ import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
import 'package:uuid/uuid.dart';
class AccountRootPage extends StatefulWidget {
const AccountRootPage({super.key});
@ -140,22 +144,23 @@ class _AccountRootPageState extends State<AccountRootPage> {
),
),
const SizedBox(height: 32),
FilledButton(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)),
UI.button(
text: 'Create new account',
onPressed: () {
if (loading) return;
_createNewAccount();
},
child: const Text('Create new account'),
big: true,
),
const SizedBox(height: 16),
FilledButton.tonal(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)),
UI.button(
text: 'Use existing account',
onPressed: () {
if (loading) return;
Navigator.push(context, MaterialPageRoute<AccountLoginPage>(builder: (context) => AccountLoginPage()));
},
child: const Text('Use existing account'),
tonal: true,
big: true,
),
],
),
@ -164,28 +169,22 @@ class _AccountRootPageState extends State<AccountRootPage> {
}
Widget _buildShowAccount(BuildContext context, AppAuth acc, User user) {
//TODO better layout
return Column(
children: [
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Padding(
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 8.0),
child: Column(
children: [
_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),
..._buildCards(context, user),
],
),
),
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 8.0),
child: Column(
children: [
_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),
..._buildCards(context, user),
SizedBox(height: 16),
_buildFooter(context, user),
SizedBox(height: 40),
],
),
const Expanded(child: SizedBox(height: 16)),
_buildFooter(context, user),
SizedBox(height: 40)
],
),
);
}
@ -272,23 +271,15 @@ class _AccountRootPageState extends State<AccountRootPage> {
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
icon: FaIcon(FontAwesomeIcons.pen),
iconSize: 18,
padding: EdgeInsets.all(4),
constraints: BoxConstraints(),
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
UI.buttonIconOnly(
onPressed: () {/*TODO*/},
icon: FontAwesomeIcons.pen,
),
const SizedBox(height: 4),
if (!user.isPro)
IconButton(
icon: FaIcon(FontAwesomeIcons.cartCircleArrowUp),
iconSize: 18,
padding: EdgeInsets.all(4),
constraints: BoxConstraints(),
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
UI.buttonIconOnly(
onPressed: () {/*TODO*/},
icon: FontAwesomeIcons.cartCircleArrowUp,
),
],
),
@ -298,132 +289,97 @@ class _AccountRootPageState extends State<AccountRootPage> {
List<Widget> _buildCards(BuildContext context, User user) {
return [
Card.filled(
UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
color: Theme.of(context).cardTheme.color,
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: () {/*TODO*/},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
FutureBuilder(
future: futureSubscriptionCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
const SizedBox(width: 12),
Text('Subscriptions', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
child: Row(
children: [
FutureBuilder(
future: futureSubscriptionCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
),
const SizedBox(width: 12),
Text('Subscriptions', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
),
onTap: () {/*TODO*/},
),
Card.filled(
UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
color: Theme.of(context).cardTheme.color,
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: () {/*TODO*/},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
FutureBuilder(
future: futureClientCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
const SizedBox(width: 12),
Text('Clients', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
child: Row(
children: [
FutureBuilder(
future: futureClientCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
),
const SizedBox(width: 12),
Text('Clients', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
),
onTap: () {/*TODO*/},
),
Card.filled(
UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
color: Theme.of(context).cardTheme.color,
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: () {/*TODO*/},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
FutureBuilder(
future: futureKeyCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
const SizedBox(width: 12),
Text('Keys', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
child: Row(
children: [
FutureBuilder(
future: futureKeyCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
),
const SizedBox(width: 12),
Text('Keys', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
),
onTap: () {/*TODO*/},
),
Card.filled(
UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
color: Theme.of(context).cardTheme.color,
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: () {/*TODO*/},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
FutureBuilder(
future: futureChannelSubscribedCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
const SizedBox(width: 12),
Text('Channels', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
child: Row(
children: [
FutureBuilder(
future: futureChannelSubscribedCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
),
const SizedBox(width: 12),
Text('Channels', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
),
onTap: () {/*TODO*/},
),
Card.filled(
UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
color: Theme.of(context).cardTheme.color,
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: () {/*TODO*/},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text('${user.messagesSent}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
const SizedBox(width: 12),
Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
),
),
child: Row(
children: [
Text('${user.messagesSent}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
const SizedBox(width: 12),
Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
),
onTap: () {/*TODO*/},
),
];
}
@ -433,9 +389,19 @@ class _AccountRootPageState extends State<AccountRootPage> {
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Row(
children: [
Expanded(child: FilledButton(onPressed: _logout, child: Text('Logout'), style: TextButton.styleFrom(backgroundColor: Colors.orange))),
Expanded(
child: UI.button(
text: 'Logout',
onPressed: _logout,
color: Colors.orange,
)),
const SizedBox(width: 8),
Expanded(child: FilledButton(onPressed: _deleteAccount, child: Text('Delete Account'), style: TextButton.styleFrom(backgroundColor: Colors.red))),
Expanded(
child: UI.button(
text: 'Delete Account',
onPressed: _deleteAccount,
color: Colors.red,
)),
],
),
);
@ -447,15 +413,21 @@ class _AccountRootPageState extends State<AccountRootPage> {
final acc = Provider.of<AppAuth>(context, listen: false);
try {
final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true);
final String? fcmToken;
if (Platform.isLinux) {
Toaster.warn("Unsupported Platform", 'Your platform is not supported by FCM - notifications will not work');
fcmToken = '(linux-' + Uuid().v4() + ')';
} else {
final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true);
if (notificationSettings.authorizationStatus == AuthorizationStatus.denied) {
Toaster.error("Missing Permission", 'Please allow notifications to create an account');
return;
if (notificationSettings.authorizationStatus == AuthorizationStatus.denied) {
Toaster.error("Missing Permission", 'Please allow notifications to create an account');
return;
}
fcmToken = await FirebaseMessaging.instance.getToken();
}
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken == null) {
Toaster.warn("Missing Token", 'No FCM Token found, please allow notifications, ensure you have a network connection and restart the app');
return;

View File

@ -9,6 +9,7 @@ import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/token_source.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class AccountLoginPage extends StatefulWidget {
const AccountLoginPage({super.key});
@ -102,10 +103,10 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
),
),
const SizedBox(height: 16),
FilledButton(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)),
UI.button(
text: 'Login',
big: true,
onPressed: _login,
child: const Text('Login'),
),
],
),

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:toastification/toastification.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class DebugActionsPage extends StatefulWidget {
@override
@ -17,36 +17,40 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
UI.button(
big: false,
onPressed: () => Toaster.success("Hello World", "This was a triumph!"),
child: const Text('Show Success Notification'),
text: 'Show Success Notification',
),
FilledButton(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
SizedBox(height: 4),
UI.button(
big: false,
onPressed: () => Toaster.info("Hello World", "This was a triumph!"),
child: const Text('Show Info Notification'),
text: 'Show Info Notification',
),
FilledButton(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
SizedBox(height: 4),
UI.button(
big: false,
onPressed: () => Toaster.warn("Hello World", "This was a triumph!"),
child: const Text('Show Warn Notification'),
text: 'Show Warn Notification',
),
FilledButton(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
SizedBox(height: 4),
UI.button(
big: false,
onPressed: () => Toaster.error("Hello World", "This was a triumph!"),
child: const Text('Show Info Notification'),
text: 'Show Info Notification',
),
FilledButton(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
SizedBox(height: 4),
UI.button(
big: false,
onPressed: () => Toaster.simple("Hello World"),
child: const Text('Show Simple Notification'),
text: 'Show Simple Notification',
),
SizedBox(height: 20),
FilledButton(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
UI.button(
big: false,
onPressed: _sendTokenToServer,
child: const Text('Send FCM Token to Server'),
text: 'Send FCM Token to Server',
),
],
),

View File

@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/state/request_log.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class DebugRequestViewPage extends StatelessWidget {
final SCNRequest request;
@ -55,17 +56,13 @@ class DebugRequestViewPage extends StatelessWidget {
Expanded(
child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
),
IconButton(
icon: FaIcon(
FontAwesomeIcons.copy,
),
UI.buttonIconOnly(
iconSize: 14,
padding: EdgeInsets.fromLTRB(0, 0, 4, 0),
constraints: BoxConstraints(),
onPressed: () {
Clipboard.setData(new ClipboardData(text: value));
Clipboard.setData(new ClipboardData(text: title));
Toaster.info("Clipboard", 'Copied text to Clipboard');
},
icon: FontAwesomeIcons.copy,
),
],
),

View File

@ -12,6 +12,9 @@ import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
class MessageListPage extends StatefulWidget {
const MessageListPage({super.key});
//TODO reload on switch to tab
//TODO reload on app to foreground
@override
State<MessageListPage> createState() => _MessageListPageState();
}
@ -25,6 +28,7 @@ class _MessageListPageState extends State<MessageListPage> {
@override
void initState() {
//TODO init with state from cache - also allow tho show cache on error
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});

View File

@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/message.dart';
import 'package:intl/intl.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class MessageListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
@ -102,19 +103,10 @@ class MessageListItem extends StatelessWidget {
if (message.priority == 2) SizedBox(width: 4),
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
if (message.priority == 0) SizedBox(width: 4),
Container(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 0),
margin: const EdgeInsets.fromLTRB(0, 0, 4, 0),
decoration: BoxDecoration(
color: Theme.of(context).hintColor,
borderRadius: BorderRadius.all(Radius.circular(4)),
),
child: Text(
resolveChannelName(message),
style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).cardColor, fontSize: 12),
overflow: TextOverflow.clip,
maxLines: 1,
),
UI.channelChip(
context: context,
text: resolveChannelName(message),
margin: EdgeInsets.fromLTRB(0, 0, 4, 0),
),
Expanded(child: SizedBox()),
Text(

View File

@ -1,9 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.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/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/api_error.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/message.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class MessageViewPage extends StatefulWidget {
const MessageViewPage({super.key, required this.message});
@ -15,18 +25,54 @@ class MessageViewPage extends StatefulWidget {
}
class _MessageViewPageState extends State<MessageViewPage> {
late Future<Message>? futureMessage;
late Future<(Message, ChannelWithSubscription?, KeyToken?)>? mainFuture;
(Message, ChannelWithSubscription?, KeyToken?)? mainFutureSnapshot = null;
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
bool _monospaceMode = false;
@override
void initState() {
super.initState();
futureMessage = fetchMessage();
mainFuture = fetchData();
}
Future<Message> fetchMessage() async {
Future<(Message, ChannelWithSubscription?, KeyToken?)> fetchData() async {
final acc = Provider.of<AppAuth>(context, listen: false);
return await APIClient.getMessage(acc, widget.message.messageID);
final msg = await APIClient.getMessage(acc, widget.message.messageID);
ChannelWithSubscription? chn = null;
try {
chn = await APIClient.getChannel(acc, msg.channelID); //TODO getShortChannel (?) -> no perm
} on APIException catch (e) {
if (e.error == APIError.USER_AUTH_FAILED) {
chn = null;
} else {
rethrow;
}
}
KeyToken? tok = null;
try {
tok = await APIClient.getKeyToken(acc, msg.usedKeyID); //TODO getShortKeyToken (?) -> no perm
} on APIException catch (e) {
if (e.error == APIError.USER_AUTH_FAILED) {
tok = null;
} else {
rethrow;
}
}
//TODO getShortUser for sender
//await Future.delayed(const Duration(seconds: 2), () {});
final r = (msg, chn, tok);
mainFutureSnapshot = r;
return r;
}
@override
@ -39,15 +85,18 @@ class _MessageViewPageState extends State<MessageViewPage> {
return SCNScaffold(
title: 'Message',
showSearch: false,
child: FutureBuilder<Message>(
future: futureMessage,
showShare: true,
onShare: _share,
child: FutureBuilder<(Message, ChannelWithSubscription?, KeyToken?)>(
future: mainFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
return buildMessageView(snapshot.data!, false);
final (msg, chn, tok) = snapshot.data!;
return _buildMessageView(context, msg, chn, tok, false);
} else if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}')); //TODO nice error page
} else if (!widget.message.trimmed) {
return buildMessageView(widget.message, true);
return _buildMessageView(context, widget.message, null, null, true);
} else {
return const Center(child: CircularProgressIndicator());
}
@ -56,15 +105,172 @@ class _MessageViewPageState extends State<MessageViewPage> {
);
}
Widget buildMessageView(Message message, bool loading) {
//TODO loading true/false indicator
return Center(
child: Column(
children: [
Text(message.title),
Text(message.content ?? ''),
],
void _share() async {
var msg = widget.message;
if (mainFutureSnapshot != null) {
(msg, _, _) = mainFutureSnapshot!;
}
if (msg.content != null) {
final result = await Share.share(msg.content!, subject: msg.title);
if (result.status == ShareResultStatus.unavailable) {
Toaster.error('Error', "Failed to open share dialog");
}
} else {
final result = await Share.share(msg.title);
if (result.status == ShareResultStatus.unavailable) {
Toaster.error('Error', "Failed to open share dialog");
}
}
}
Widget _buildMessageView(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) {
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
..._buildMessageHeader(context, message, channel, token, loading),
SizedBox(height: 8),
if (message.content != null) ..._buildMessageContent(context, message, channel, token),
SizedBox(height: 8),
if (message.senderName != null) _buildMetaCard(context, FontAwesomeIcons.solidSignature, 'Sender', [message.senderName!], () => {/*TODO*/}),
_buildMetaCard(context, FontAwesomeIcons.solidGearCode, 'KeyToken', [message.usedKeyID, if (token != null) token.name], () => {/*TODO*/}),
_buildMetaCard(context, FontAwesomeIcons.solidIdCardClip, 'MessageID', [message.messageID, if (message.userMessageID != null) message.userMessageID!], null),
_buildMetaCard(context, FontAwesomeIcons.solidSnake, 'Channel', [message.channelID, channel?.channel.displayName ?? message.channelInternalName], () => {/*TODO*/}),
_buildMetaCard(context, FontAwesomeIcons.solidTimer, 'Timestamp', [message.timestamp], null),
_buildMetaCard(context, FontAwesomeIcons.solidUser, 'User', ['TODO'], () => {/*TODO*/}), //TODO
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
],
),
),
);
}
String _resolveChannelName(ChannelWithSubscription? channel, Message message) {
return channel?.channel.displayName ?? message.channelInternalName;
}
List<Widget> _buildMessageHeader(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) {
return [
Row(
children: [
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)),
],
),
SizedBox(height: 8),
if (!loading) Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
if (loading)
Stack(
children: [
Row(
children: [
Flexible(child: Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold))),
SizedBox(height: 20, width: 20),
],
),
Row(
children: [
Expanded(child: SizedBox(width: 0)),
SizedBox(child: CircularProgressIndicator(), height: 20, width: 20),
],
),
],
),
];
}
List<Widget> _buildMessageContent(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) {
return [
Row(
children: [
if (message.priority == 2) FaIcon(FontAwesomeIcons.solidTriangleExclamation, size: 16, color: Colors.red[900]),
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
Expanded(child: SizedBox()),
UI.buttonIconOnly(
onPressed: () {
Clipboard.setData(new ClipboardData(text: message.content ?? ''));
Toaster.info("Clipboard", 'Copied text to Clipboard');
},
icon: FontAwesomeIcons.copy,
),
UI.buttonIconOnly(
icon: _monospaceMode ? FontAwesomeIcons.lineColumns : FontAwesomeIcons.alignLeft,
onPressed: () {
setState(() {
_monospaceMode = !_monospaceMode;
});
},
),
],
),
_monospaceMode
? UI.box(
context: context,
padding: const EdgeInsets.all(4),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text(
message.content ?? '',
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"]),
),
),
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,
)
];
}
Widget _buildMetaCard(BuildContext context, IconData icn, String title, List<String> values, void Function()? action) {
final container = UI.box(
context: context,
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
child: Row(
children: [
FaIcon(icn, size: 18),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
for (final val in values) Text(val, style: const TextStyle(fontSize: 14)),
],
),
],
),
);
if (action == null) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: container,
);
} else {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: action,
child: container,
),
);
}
}
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/models/client.dart';
@ -63,6 +62,8 @@ class AppAuth extends ChangeNotifier implements TokenSource {
}
void load() {
//final cdat = Globals().sharedPrefs.getString('auth.cdate');
//final mdat = Globals().sharedPrefs.getString('auth.mdate');
final uid = Globals().sharedPrefs.getString('auth.userid');
final cid = Globals().sharedPrefs.getString('auth.clientid');
final toka = Globals().sharedPrefs.getString('auth.tokenadmin');
@ -85,17 +86,23 @@ class AppAuth extends ChangeNotifier implements TokenSource {
}
Future<void> save() async {
final prefs = await SharedPreferences.getInstance();
if (_clientID == null || _userID == null || _tokenAdmin == null || _tokenSend == null) {
await prefs.remove('auth.userid');
await prefs.remove('auth.tokenadmin');
await prefs.remove('auth.tokensend');
await Globals().sharedPrefs.remove('auth.userid');
await Globals().sharedPrefs.remove('auth.clientid');
await Globals().sharedPrefs.remove('auth.tokenadmin');
await Globals().sharedPrefs.remove('auth.tokensend');
await Globals().sharedPrefs.setString('auth.cdate', "");
await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
} else {
await prefs.setString('auth.userid', _userID!);
await prefs.setString('auth.clientid', _clientID!);
await prefs.setString('auth.tokenadmin', _tokenAdmin!);
await prefs.setString('auth.tokensend', _tokenSend!);
await Globals().sharedPrefs.setString('auth.userid', _userID!);
await Globals().sharedPrefs.setString('auth.clientid', _clientID!);
await Globals().sharedPrefs.setString('auth.tokenadmin', _tokenAdmin!);
await Globals().sharedPrefs.setString('auth.tokensend', _tokenSend!);
if (Globals().sharedPrefs.getString('auth.cdate') == null) await Globals().sharedPrefs.setString('auth.cdate', DateTime.now().toIso8601String());
await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
}
Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
}
Future<User> loadUser({bool force = false}) async {

View File

@ -0,0 +1 @@
class Navi {}

109
flutter/lib/utils/ui.dart Normal file
View File

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class UI {
static const double DefaultBorderRadius = 4;
static Widget button({required String text, required void Function() onPressed, bool big = false, Color? color = null, bool tonal = false, IconData? icon = null}) {
final double fontSize = big ? 24 : 14;
final padding = big ? EdgeInsets.fromLTRB(8, 12, 8, 12) : null;
final style = FilledButton.styleFrom(
textStyle: TextStyle(fontSize: fontSize),
padding: padding,
backgroundColor: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
);
if (tonal) {
if (icon != null) {
return FilledButton.tonalIcon(
style: style,
onPressed: onPressed,
icon: Icon(icon),
label: Text(text),
);
} else {
return FilledButton.tonal(
style: style,
onPressed: onPressed,
child: Text(text),
);
}
} else {
if (icon != null) {
return FilledButton.icon(
style: style,
onPressed: onPressed,
icon: Icon(icon),
label: Text(text),
);
} else {
return FilledButton(
style: style,
onPressed: onPressed,
child: Text(text),
);
}
}
}
static Widget buttonIconOnly({
required void Function() onPressed,
required IconData icon,
double? iconSize = null,
}) {
return IconButton(
icon: FaIcon(icon),
iconSize: iconSize ?? 18,
padding: EdgeInsets.all(4),
constraints: BoxConstraints(),
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
onPressed: onPressed,
);
}
static Widget buttonCard({required BuildContext context, required Widget child, required void Function() onTap, EdgeInsets? margin = null}) {
return Card.filled(
margin: margin,
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
color: Theme.of(context).cardTheme.color,
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
);
}
static Widget channelChip({required BuildContext context, required String text, EdgeInsets? margin = null, double fontSize = 12}) {
return Container(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 0),
margin: margin,
decoration: BoxDecoration(
color: Theme.of(context).hintColor,
borderRadius: BorderRadius.all(Radius.circular(DefaultBorderRadius)),
),
child: Text(
text,
style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).cardColor, fontSize: fontSize),
overflow: TextOverflow.clip,
maxLines: 1,
),
);
}
static Widget box({required BuildContext context, required Widget child, required EdgeInsets? padding, Color? borderColor = null}) {
return Container(
padding: padding ?? EdgeInsets.all(4),
decoration: BoxDecoration(
border: Border.all(color: borderColor ?? Theme.of(context).hintColor),
borderRadius: BorderRadius.circular(DefaultBorderRadius),
),
child: child,
);
}
}

View File

@ -10,6 +10,7 @@ import firebase_core
import firebase_messaging
import package_info_plus
import path_provider_foundation
import share_plus
import shared_preferences_foundation
import url_launcher_macos
@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@ -161,6 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
url: "https://pub.dev"
source: hosted
version: "0.3.4+1"
crypto:
dependency: transitive
description:
@ -687,6 +695,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544
url: "https://pub.dev"
source: hosted
version: "9.0.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
shared_preferences:
dependency: "direct main"
description:
@ -941,7 +965,7 @@ packages:
source: hosted
version: "3.1.1"
uuid:
dependency: transitive
dependency: "direct main"
description:
name: uuid
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"

View File

@ -29,6 +29,8 @@ dependencies:
firebase_messaging: ^14.9.4
device_info_plus: ^10.1.0
toastification: ^2.0.0
uuid: ^4.4.0
share_plus: ^9.0.0
dependency_overrides:

View File

@ -7,11 +7,14 @@
#include "generated_plugin_registrant.h"
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
firebase_core
share_plus
url_launcher_windows
)

View File

@ -0,0 +1,127 @@
package handler
import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"github.com/gin-gonic/gin"
"net/http"
)
// GetUserPreview swaggerdoc
//
// @Summary Get a user (similar to api-user-get, but can be called from anyone and only returns a subset of fields)
// @ID api-user-get-preview
// @Tags API-v2
//
// @Param uid path string true "UserID"
//
// @Success 200 {object} models.UserPreviewJSON
// @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 "user not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/preview/users/{uid} [GET]
func (h APIHandler) GetUserPreview(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
}
var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
user, err := h.database.GetUser(ctx, u.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 ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSONPreview()))
}
// GetChannelPreview swaggerdoc
//
// @Summary Get a single channel (similar to api-channels-get, but can be called from anyone and only returns a subset of fields)
// @ID api-channels-get-preview
// @Tags API-v2
//
// @Param cid path string true "ChannelID"
//
// @Success 200 {object} models.ChannelPreviewJSON
// @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 "channel not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/preview/channels/{cid} [GET]
func (h APIHandler) GetChannelPreview(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
ChannelID models.ChannelID `uri:"cid" binding:"entityid"`
}
var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
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)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSONPreview()))
}
// GetUserKeyPreview swaggerdoc
//
// @Summary Get a single key (similar to api-tokenkeys-get, but can be called from anyone and only returns a subset of fields)
// @ID api-tokenkeys-get-preview
// @Tags API-v2
//
// @Param kid path string true "TokenKeyID"
//
// @Success 200 {object} models.KeyTokenPreviewJSON
// @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/preview/keys/{kid} [GET]
func (h APIHandler) GetUserKeyPreview(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
}
var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, keytoken.JSONPreview()))
}

View File

@ -157,6 +157,10 @@ func (r *Router) Init(e *gin.Engine) error {
apiv2.GET("/messages", r.Wrap(r.apiHandler.ListMessages))
apiv2.GET("/messages/:mid", r.Wrap(r.apiHandler.GetMessage))
apiv2.DELETE("/messages/:mid", r.Wrap(r.apiHandler.DeleteMessage))
apiv2.GET("/preview/users/:uid", r.Wrap(r.apiHandler.GetUserPreview))
apiv2.GET("/preview/keys/:kid", r.Wrap(r.apiHandler.GetUserKeyPreview))
apiv2.GET("/preview/channels/:cid", r.Wrap(r.apiHandler.GetChannelPreview))
}
// ================ Send API (unversioned) ================

View File

@ -41,6 +41,16 @@ func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription {
}
}
func (c Channel) JSONPreview() ChannelPreviewJSON {
return ChannelPreviewJSON{
ChannelID: c.ChannelID,
OwnerUserID: c.OwnerUserID,
InternalName: c.InternalName,
DisplayName: c.DisplayName,
DescriptionName: c.DescriptionName,
}
}
type ChannelWithSubscription struct {
Channel
Subscription *Subscription
@ -74,6 +84,14 @@ type ChannelWithSubscriptionJSON struct {
Subscription *SubscriptionJSON `json:"subscription"`
}
type ChannelPreviewJSON struct {
ChannelID ChannelID `json:"channel_id"`
OwnerUserID UserID `json:"owner_user_id"`
InternalName string `json:"internal_name"`
DisplayName string `json:"display_name"`
DescriptionName *string `json:"description_name"`
}
type ChannelDB struct {
ChannelID ChannelID `db:"channel_id"`
OwnerUserID UserID `db:"owner_user_id"`

View File

@ -5,7 +5,7 @@ package models
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.com/BlackForestBytes/goext/enums"
const ChecksumEnumGenerator = "5b115c5f107801af608630d2c5adce57cd4b050d176c8cd3db5c132020bf153c" // GoExtVersion: 0.0.463
const ChecksumEnumGenerator = "e500346e3f60b3abf78558ec3df128c3be2a1cefa71c4f1feba9293d14eb85d1" // GoExtVersion: 0.0.463
// ================================ ClientType ================================
//

View File

@ -15,7 +15,7 @@ import "reflect"
import "regexp"
import "strings"
const ChecksumCharsetIDGenerator = "5b115c5f107801af608630d2c5adce57cd4b050d176c8cd3db5c132020bf153c" // GoExtVersion: 0.0.463
const ChecksumCharsetIDGenerator = "e500346e3f60b3abf78558ec3df128c3be2a1cefa71c4f1feba9293d14eb85d1" // GoExtVersion: 0.0.463
const idlen = 24

View File

@ -92,6 +92,17 @@ func (k KeyToken) JSON() KeyTokenJSON {
}
}
func (k KeyToken) JSONPreview() KeyTokenPreviewJSON {
return KeyTokenPreviewJSON{
KeyTokenID: k.KeyTokenID,
Name: k.Name,
OwnerUserID: k.OwnerUserID,
AllChannels: k.AllChannels,
Channels: k.Channels,
Permissions: k.Permissions.String(),
}
}
type KeyTokenJSON struct {
KeyTokenID KeyTokenID `json:"keytoken_id"`
Name string `json:"name"`
@ -109,6 +120,15 @@ type KeyTokenWithTokenJSON struct {
Token string `json:"token"`
}
type KeyTokenPreviewJSON struct {
KeyTokenID KeyTokenID `json:"keytoken_id"`
Name string `json:"name"`
OwnerUserID UserID `json:"owner_user_id"`
AllChannels bool `json:"all_channels"`
Channels []ChannelID `json:"channels"`
Permissions string `json:"permissions"`
}
func (j KeyTokenJSON) WithToken(tok string) KeyTokenWithTokenJSON {
return KeyTokenWithTokenJSON{
KeyTokenJSON: j,

View File

@ -116,6 +116,13 @@ func (u User) MaxTimestampDiffHours() int {
return 24
}
func (u User) JSONPreview() UserPreviewJSON {
return UserPreviewJSON{
UserID: u.UserID,
Username: u.Username,
}
}
type UserJSON struct {
UserID UserID `json:"user_id"`
Username *string `json:"username"`
@ -137,6 +144,11 @@ type UserJSON struct {
MaxUserMessageIDLength int `json:"max_user_message_id_length"`
}
type UserPreviewJSON struct {
UserID UserID `json:"user_id"`
Username *string `json:"username"`
}
type UserJSONWithClientsAndKeys struct {
UserJSON
Clients []ClientJSON `json:"clients"`

View File

@ -19,37 +19,61 @@
"parameters": [
{
"type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content",
"in": "query"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id",
"in": "query"
},
{
"enum": [
0,
1,
2
],
"type": "integer",
"example": 1,
"name": "priority",
"in": "query"
},
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{
"type": "number",
"example": 1669824037,
"name": "timestamp",
"in": "query"
},
{
"type": "string",
"example": "Hello World",
"name": "title",
"in": "query"
},
{
"type": "integer",
"name": "user_id",
"in": "query"
},
{
"type": "string",
"name": "user_key",
"example": "7725",
"name": "user_id",
"in": "query"
},
{
@ -62,37 +86,61 @@
},
{
"type": "string",
"example": "test",
"name": "channel",
"in": "formData"
},
{
"type": "string",
"example": "This is a message",
"name": "content",
"in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id",
"in": "formData"
},
{
"enum": [
0,
1,
2
],
"type": "integer",
"example": 1,
"name": "priority",
"in": "formData"
},
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{
"type": "number",
"example": 1669824037,
"name": "timestamp",
"in": "formData"
},
{
"type": "string",
"example": "Hello World",
"name": "title",
"in": "formData"
},
{
"type": "integer",
"name": "user_id",
"in": "formData"
},
{
"type": "string",
"name": "user_key",
"example": "7725",
"name": "user_id",
"in": "formData"
}
],
@ -1009,6 +1057,156 @@
}
}
},
"/api/v2/preview/channels/{cid}": {
"get": {
"tags": [
"API-v2"
],
"summary": "Get a single channel (similar to api-channels-get, but can be called from anyone and only returns a subset of fields)",
"operationId": "api-channels-get-preview",
"parameters": [
{
"type": "string",
"description": "ChannelID",
"name": "cid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.ChannelPreviewJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "channel not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/preview/keys/{kid}": {
"get": {
"tags": [
"API-v2"
],
"summary": "Get a single key (similar to api-tokenkeys-get, but can be called from anyone and only returns a subset of fields)",
"operationId": "api-tokenkeys-get-preview",
"parameters": [
{
"type": "string",
"description": "TokenKeyID",
"name": "kid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.KeyTokenPreviewJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/preview/users/{uid}": {
"get": {
"tags": [
"API-v2"
],
"summary": "Get a user (similar to api-user-get, but can be called from anyone and only returns a subset of fields)",
"operationId": "api-user-get-preview",
"parameters": [
{
"type": "string",
"description": "UserID",
"name": "uid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.UserPreviewJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/users": {
"post": {
"tags": [
@ -2567,37 +2765,61 @@
"parameters": [
{
"type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content",
"in": "query"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id",
"in": "query"
},
{
"enum": [
0,
1,
2
],
"type": "integer",
"example": 1,
"name": "priority",
"in": "query"
},
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{
"type": "number",
"example": 1669824037,
"name": "timestamp",
"in": "query"
},
{
"type": "string",
"example": "Hello World",
"name": "title",
"in": "query"
},
{
"type": "integer",
"name": "user_id",
"in": "query"
},
{
"type": "string",
"name": "user_key",
"example": "7725",
"name": "user_id",
"in": "query"
},
{
@ -2610,37 +2832,61 @@
},
{
"type": "string",
"example": "test",
"name": "channel",
"in": "formData"
},
{
"type": "string",
"example": "This is a message",
"name": "content",
"in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id",
"in": "formData"
},
{
"enum": [
0,
1,
2
],
"type": "integer",
"example": 1,
"name": "priority",
"in": "formData"
},
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{
"type": "number",
"example": 1669824037,
"name": "timestamp",
"in": "formData"
},
{
"type": "string",
"example": "Hello World",
"name": "title",
"in": "formData"
},
{
"type": "integer",
"name": "user_id",
"in": "formData"
},
{
"type": "string",
"name": "user_key",
"example": "7725",
"name": "user_id",
"in": "formData"
}
],
@ -2689,72 +2935,120 @@
"parameters": [
{
"type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content",
"in": "query"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id",
"in": "query"
},
{
"enum": [
0,
1,
2
],
"type": "integer",
"example": 1,
"name": "priority",
"in": "query"
},
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{
"type": "number",
"example": 1669824037,
"name": "timestamp",
"in": "query"
},
{
"type": "string",
"example": "Hello World",
"name": "title",
"in": "query"
},
{
"type": "integer",
"type": "string",
"example": "7725",
"name": "user_id",
"in": "query"
},
{
"type": "string",
"name": "user_key",
"in": "query"
"example": "test",
"name": "channel",
"in": "formData"
},
{
"type": "string",
"example": "This is a message",
"name": "content",
"in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id",
"in": "formData"
},
{
"enum": [
0,
1,
2
],
"type": "integer",
"example": 1,
"name": "priority",
"in": "formData"
},
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{
"type": "number",
"example": 1669824037,
"name": "timestamp",
"in": "formData"
},
{
"type": "string",
"example": "Hello World",
"name": "title",
"in": "formData"
},
{
"type": "integer",
"name": "user_id",
"in": "formData"
},
{
"type": "string",
"name": "user_key",
"example": "7725",
"name": "user_id",
"in": "formData"
}
],
@ -3251,26 +3545,46 @@
"handler.SendMessage.combined": {
"type": "object",
"properties": {
"channel": {
"type": "string",
"example": "test"
},
"content": {
"type": "string"
"type": "string",
"example": "This is a message"
},
"key": {
"type": "string",
"example": "P3TNH8mvv14fm"
},
"msg_id": {
"type": "string"
"type": "string",
"example": "db8b0e6a-a08c-4646"
},
"priority": {
"type": "integer"
"type": "integer",
"enum": [
0,
1,
2
],
"example": 1
},
"sender_name": {
"type": "string",
"example": "example-server"
},
"timestamp": {
"type": "number"
"type": "number",
"example": 1669824037
},
"title": {
"type": "string"
"type": "string",
"example": "Hello World"
},
"user_id": {
"type": "integer"
},
"user_key": {
"type": "string"
"type": "string",
"example": "7725"
}
}
},
@ -3299,7 +3613,7 @@
"type": "integer"
},
"scn_msg_id": {
"type": "integer"
"type": "string"
},
"success": {
"type": "boolean"
@ -3487,6 +3801,26 @@
}
}
},
"models.ChannelPreviewJSON": {
"type": "object",
"properties": {
"channel_id": {
"type": "string"
},
"description_name": {
"type": "string"
},
"display_name": {
"type": "string"
},
"internal_name": {
"type": "string"
},
"owner_user_id": {
"type": "string"
}
}
},
"models.ChannelWithSubscriptionJSON": {
"type": "object",
"properties": {
@ -3630,6 +3964,32 @@
}
}
},
"models.KeyTokenPreviewJSON": {
"type": "object",
"properties": {
"all_channels": {
"type": "boolean"
},
"channels": {
"type": "array",
"items": {
"type": "string"
}
},
"keytoken_id": {
"type": "string"
},
"name": {
"type": "string"
},
"owner_user_id": {
"type": "string"
},
"permissions": {
"type": "string"
}
}
},
"models.KeyTokenWithTokenJSON": {
"type": "object",
"properties": {
@ -3870,6 +4230,17 @@
"type": "string"
}
}
},
"models.UserPreviewJSON": {
"type": "object",
"properties": {
"user_id": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
},
"tags": [

View File

@ -327,19 +327,36 @@ definitions:
type: object
handler.SendMessage.combined:
properties:
channel:
example: test
type: string
content:
example: This is a message
type: string
key:
example: P3TNH8mvv14fm
type: string
msg_id:
example: db8b0e6a-a08c-4646
type: string
priority:
enum:
- 0
- 1
- 2
example: 1
type: integer
sender_name:
example: example-server
type: string
timestamp:
example: 1669824037
type: number
title:
example: Hello World
type: string
user_id:
type: integer
user_key:
example: "7725"
type: string
type: object
handler.SendMessage.response:
@ -359,7 +376,7 @@ definitions:
quota_max:
type: integer
scn_msg_id:
type: integer
type: string
success:
type: boolean
suppress_send:
@ -480,6 +497,19 @@ definitions:
uri:
type: string
type: object
models.ChannelPreviewJSON:
properties:
channel_id:
type: string
description_name:
type: string
display_name:
type: string
internal_name:
type: string
owner_user_id:
type: string
type: object
models.ChannelWithSubscriptionJSON:
properties:
channel_id:
@ -577,6 +607,23 @@ definitions:
timestamp_lastused:
type: string
type: object
models.KeyTokenPreviewJSON:
properties:
all_channels:
type: boolean
channels:
items:
type: string
type: array
keytoken_id:
type: string
name:
type: string
owner_user_id:
type: string
permissions:
type: string
type: object
models.KeyTokenWithTokenJSON:
properties:
all_channels:
@ -736,6 +783,13 @@ definitions:
username:
type: string
type: object
models.UserPreviewJSON:
properties:
user_id:
type: string
username:
type: string
type: object
host: simplecloudnotifier.de
info:
contact: {}
@ -748,52 +802,90 @@ paths:
description: All parameter can be set via query-parameter or the json body.
Only UserID, UserKey and Title are required
parameters:
- in: query
- example: test
in: query
name: channel
type: string
- example: This is a message
in: query
name: content
type: string
- in: query
- example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id
type: string
- in: query
- enum:
- 0
- 1
- 2
example: 1
in: query
name: priority
type: integer
- in: query
- example: example-server
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp
type: number
- in: query
- example: Hello World
in: query
name: title
type: string
- in: query
- example: "7725"
in: query
name: user_id
type: integer
- in: query
name: user_key
type: string
- description: ' '
in: body
name: post_body
schema:
$ref: '#/definitions/handler.SendMessage.combined'
- in: formData
- example: test
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content
type: string
- in: formData
- example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id
type: string
- in: formData
- enum:
- 0
- 1
- 2
example: 1
in: formData
name: priority
type: integer
- in: formData
- example: example-server
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp
type: number
- in: formData
- example: Hello World
in: formData
name: title
type: string
- in: formData
- example: "7725"
in: formData
name: user_id
type: integer
- in: formData
name: user_key
type: string
responses:
"200":
@ -1422,6 +1514,108 @@ paths:
summary: Get a single message (untrimmed)
tags:
- API-v2
/api/v2/preview/channels/{cid}:
get:
operationId: api-channels-get-preview
parameters:
- description: ChannelID
in: path
name: cid
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.ChannelPreviewJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: channel not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Get a single channel (similar to api-channels-get, but can be called
from anyone and only returns a subset of fields)
tags:
- API-v2
/api/v2/preview/keys/{kid}:
get:
operationId: api-tokenkeys-get-preview
parameters:
- description: TokenKeyID
in: path
name: kid
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.KeyTokenPreviewJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Get a single key (similar to api-tokenkeys-get, but can be called from
anyone and only returns a subset of fields)
tags:
- API-v2
/api/v2/preview/users/{uid}:
get:
operationId: api-user-get-preview
parameters:
- description: UserID
in: path
name: uid
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.UserPreviewJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: user not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Get a user (similar to api-user-get, but can be called from anyone
and only returns a subset of fields)
tags:
- API-v2
/api/v2/users:
post:
operationId: api-user-create
@ -2491,52 +2685,90 @@ paths:
description: All parameter can be set via query-parameter or the json body.
Only UserID, UserKey and Title are required
parameters:
- in: query
- example: test
in: query
name: channel
type: string
- example: This is a message
in: query
name: content
type: string
- in: query
- example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id
type: string
- in: query
- enum:
- 0
- 1
- 2
example: 1
in: query
name: priority
type: integer
- in: query
- example: example-server
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp
type: number
- in: query
- example: Hello World
in: query
name: title
type: string
- in: query
- example: "7725"
in: query
name: user_id
type: integer
- in: query
name: user_key
type: string
- description: ' '
in: body
name: post_body
schema:
$ref: '#/definitions/handler.SendMessage.combined'
- in: formData
- example: test
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content
type: string
- in: formData
- example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id
type: string
- in: formData
- enum:
- 0
- 1
- 2
example: 1
in: formData
name: priority
type: integer
- in: formData
- example: example-server
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp
type: number
- in: formData
- example: Hello World
in: formData
name: title
type: string
- in: formData
- example: "7725"
in: formData
name: user_id
type: integer
- in: formData
name: user_key
type: string
responses:
"200":
@ -2569,47 +2801,85 @@ paths:
description: All parameter can be set via query-parameter or form-data body.
Only UserID, UserKey and Title are required
parameters:
- in: query
- example: test
in: query
name: channel
type: string
- example: This is a message
in: query
name: content
type: string
- in: query
- example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id
type: string
- in: query
- enum:
- 0
- 1
- 2
example: 1
in: query
name: priority
type: integer
- in: query
- example: example-server
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp
type: number
- in: query
- example: Hello World
in: query
name: title
type: string
- in: query
- example: "7725"
in: query
name: user_id
type: integer
- in: query
name: user_key
type: string
- in: formData
- example: test
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content
type: string
- in: formData
- example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id
type: string
- in: formData
- enum:
- 0
- 1
- 2
example: 1
in: formData
name: priority
type: integer
- in: formData
- example: example-server
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp
type: number
- in: formData
- example: Hello World
in: formData
name: title
type: string
- in: formData
- example: "7725"
in: formData
name: user_id
type: integer
- in: formData
name: user_key
type: string
responses:
"200":