Hive, requestlog, etc

This commit is contained in:
2024-05-25 18:09:39 +02:00
parent 227d7871c2
commit 7e347a70c2
23 changed files with 1149 additions and 355 deletions

View File

@@ -1,8 +1,13 @@
import 'dart:convert';
import 'package:fl_toast/fl_toast.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:simplecloudnotifier/models/api_error.dart';
import 'package:simplecloudnotifier/models/key_token_auth.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/request_log.dart';
import '../models/channel.dart';
import '../models/message.dart';
@@ -21,69 +26,141 @@ enum ChannelSelector {
class APIClient {
static const String _base = 'https://simplecloudnotifier.de/api/v2';
static Future<bool> verifyToken(String uid, String tok) async {
final uri = Uri.parse('$_base/users/$uid');
final response = await http.get(uri, headers: {'Authorization': 'SCN $tok'});
static Future<T> _request<T>({
required String name,
required String method,
required String relURL,
Map<String, String>? query,
required T Function(Map<String, dynamic> json)? fn,
dynamic jsonBody,
KeyTokenAuth? auth,
Map<String, String>? header,
}) async {
final t0 = DateTime.now();
return (response.statusCode == 200);
}
final uri = Uri.parse('$_base/$relURL').replace(queryParameters: query ?? {});
static Future<User> getUser(String uid, String tok) async {
final uri = Uri.parse('$_base/users/$uid');
final response = await http.get(uri, headers: {'Authorization': 'SCN $tok'});
final req = http.Request(method, uri);
if (response.statusCode != 200) {
throw Exception('API request failed');
if (jsonBody != null) {
req.body = jsonEncode(jsonBody);
req.headers['Content-Type'] = 'application/json';
}
return User.fromJson(jsonDecode(response.body));
if (auth != null) {
req.headers['Authorization'] = 'SCN ${auth.token}';
}
req.headers['User-Agent'] = 'simplecloudnotifier/flutter/${Globals().platform.replaceAll(' ', '_')} ${Globals().version}+${Globals().buildNumber}';
if (header != null && !header.isEmpty) {
req.headers.addAll(header);
}
int responseStatusCode = 0;
String responseBody = '';
Map<String, String> responseHeaders = {};
try {
final response = await req.send();
responseBody = await response.stream.bytesToString();
responseStatusCode = response.statusCode;
responseHeaders = response.headers;
} catch (exc, trace) {
RequestLog.addRequestException(name, t0, method, uri, req.body, req.headers, exc, trace);
showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context);
rethrow;
}
if (responseStatusCode != 200) {
try {
final apierr = APIError.fromJson(jsonDecode(responseBody));
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context);
throw Exception(apierr.message);
} catch (_) {}
RequestLog.addRequestErrorStatuscode(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context);
throw Exception('API request failed with status code ${responseStatusCode}');
}
try {
final data = jsonDecode(responseBody);
if (fn != null) {
final result = fn(data);
RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
return result;
} else {
RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
return null as T;
}
} catch (exc, trace) {
RequestLog.addRequestDecodeError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, exc, trace);
showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context);
rethrow;
}
}
// ==========================================================================================================================================================
static Future<bool> verifyToken(String uid, String tok) async {
try {
await _request<void>(
name: 'verifyToken',
method: 'GET',
relURL: '/users/$uid',
fn: null,
auth: KeyTokenAuth(userId: uid, token: tok),
);
return true;
} catch (e) {
return false;
}
}
static Future<User> getUser(KeyTokenAuth auth, String uid) async {
return await _request(
name: 'getUser',
method: 'GET',
relURL: 'users/$uid',
fn: User.fromJson,
auth: auth,
);
}
static Future<List<ChannelWithSubscription>> getChannelList(KeyTokenAuth auth, ChannelSelector sel) async {
var url = '$_base/users/${auth.userId}/channels?selector=${sel.apiKey}';
final uri = Uri.parse(url);
final response = await http.get(uri, headers: {'Authorization': 'SCN ${auth.token}'});
if (response.statusCode != 200) {
throw Exception('API request failed');
}
final data = jsonDecode(response.body);
return data['channels'].map<ChannelWithSubscription>((e) => ChannelWithSubscription.fromJson(e)).toList() as List<ChannelWithSubscription>;
return await _request(
name: 'getChannelList',
method: 'GET',
relURL: 'users/${auth.userId}/channels',
query: {'selector': sel.apiKey},
fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels']),
auth: auth,
);
}
static Future<(String, List<Message>)> getMessageList(KeyTokenAuth auth, String pageToken, int? pageSize) async {
var url = '$_base/messages?next_page_token=$pageToken';
if (pageSize != null) {
url += '&page_size=$pageSize';
}
final uri = Uri.parse(url);
final response = await http.get(uri, headers: {'Authorization': 'SCN ${auth.token}'});
if (response.statusCode != 200) {
throw Exception('API request failed');
}
final data = jsonDecode(response.body);
final npt = data['next_page_token'] as String;
final messages = data['messages'].map<Message>((e) => Message.fromJson(e)).toList() as List<Message>;
return (npt, messages);
return await _request(
name: 'getMessageList',
method: 'GET',
relURL: 'messages',
query: {'next_page_token': pageToken, if (pageSize != null) 'page_size': pageSize.toString()},
fn: (json) => Message.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
auth: auth,
);
}
static Future<Message> getMessage(KeyTokenAuth auth, String msgid) async {
final uri = Uri.parse('$_base/messages/$msgid');
final response = await http.get(uri, headers: {'Authorization': 'SCN ${auth.token}'});
if (response.statusCode != 200) {
throw Exception('API request failed');
}
return Message.fromJson(jsonDecode(response.body));
return await _request(
name: 'getMessage',
method: 'GET',
relURL: 'messages/$msgid',
query: {},
fn: Message.fromJson,
auth: auth,
);
}
}

View File

@@ -1,13 +1,34 @@
import 'package:fl_toast/fl_toast.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/state/database.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:simplecloudnotifier/nav_layout.dart';
import 'package:simplecloudnotifier/state/app_theme.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/request_log.dart';
import 'package:simplecloudnotifier/state/user_account.dart';
void main() async {
await SCNDatabase.create();
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await Globals().init();
Hive.registerAdapter(SCNRequestAdapter());
Hive.registerAdapter(SCNLogAdapter());
try {
await Hive.openBox<SCNRequest>('scn-requests');
await Hive.openBox<SCNLog>('scn-logs');
} catch (e) {
print(e);
Hive.deleteBoxFromDisk('scn-requests');
Hive.deleteBoxFromDisk('scn-logs');
await Hive.openBox<SCNRequest>('scn-requests');
await Hive.openBox<SCNLog>('scn-logs');
}
runApp(
MultiProvider(
@@ -31,7 +52,7 @@ class SCNApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
Provider.of<UserAccount>(context); // ensure UserAccount is loaded
Provider.of<UserAccount>(context); // ensure UserAccount is loaded (unneccessary if lazy: false is set in MultiProvider ??)
return Consumer<AppTheme>(
builder: (context, appTheme, child) => MaterialApp(
@@ -40,7 +61,7 @@ class SCNApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light),
useMaterial3: true,
),
home: const SCNNavLayout(),
home: const ToastProvider(child: SCNNavLayout()),
),
);
}

View File

@@ -0,0 +1,22 @@
class APIError {
final String success;
final String error;
final String errhighlight;
final String message;
const APIError({
required this.success,
required this.error,
required this.errhighlight,
required this.message,
});
factory APIError.fromJson(Map<String, dynamic> json) {
return APIError(
success: json['success'],
error: json['error'],
errhighlight: json['errhighlight'],
message: json['message'],
);
}
}

View File

@@ -24,31 +24,17 @@ class Channel {
});
factory Channel.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'channel_id': String channelID,
'owner_user_id': String ownerUserID,
'internal_name': String internalName,
'display_name': String displayName,
'description_name': String? descriptionName,
'subscribe_key': String? subscribeKey,
'timestamp_created': String timestampCreated,
'timestamp_lastsent': String? timestampLastSent,
'messages_sent': int messagesSent,
} =>
Channel(
channelID: channelID,
ownerUserID: ownerUserID,
internalName: internalName,
displayName: displayName,
descriptionName: descriptionName,
subscribeKey: subscribeKey,
timestampCreated: timestampCreated,
timestampLastSent: timestampLastSent,
messagesSent: messagesSent,
),
_ => throw const FormatException('Failed to decode Channel.'),
};
return Channel(
channelID: json['channel_id'],
ownerUserID: json['owner_user_id'],
internalName: json['internal_name'],
displayName: json['display_name'],
descriptionName: json['description_name'],
subscribeKey: json['subscribe_key'],
timestampCreated: json['timestamp_created'],
timestampLastSent: json['timestamp_lastsent'],
messagesSent: json['messages_sent'],
);
}
}
@@ -69,32 +55,21 @@ class ChannelWithSubscription extends Channel {
});
factory ChannelWithSubscription.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'channel_id': String channelID,
'owner_user_id': String ownerUserID,
'internal_name': String internalName,
'display_name': String displayName,
'description_name': String? descriptionName,
'subscribe_key': String? subscribeKey,
'timestamp_created': String timestampCreated,
'timestamp_lastsent': String? timestampLastSent,
'messages_sent': int messagesSent,
'subscription': dynamic subscription,
} =>
ChannelWithSubscription(
channelID: channelID,
ownerUserID: ownerUserID,
internalName: internalName,
displayName: displayName,
descriptionName: descriptionName,
subscribeKey: subscribeKey,
timestampCreated: timestampCreated,
timestampLastSent: timestampLastSent,
messagesSent: messagesSent,
subscription: Subscription.fromJson(subscription),
),
_ => throw const FormatException('Failed to decode Channel.'),
};
return ChannelWithSubscription(
channelID: json['channel_id'],
ownerUserID: json['owner_user_id'],
internalName: json['internal_name'],
displayName: json['display_name'],
descriptionName: json['description_name'],
subscribeKey: json['subscribe_key'],
timestampCreated: json['timestamp_created'],
timestampLastSent: json['timestamp_lastsent'],
messagesSent: json['messages_sent'],
subscription: Subscription.fromJson(json['subscription']),
);
}
static List<ChannelWithSubscription> fromJsonArray(List<dynamic> jsonArr) {
return jsonArr.map<ChannelWithSubscription>((e) => ChannelWithSubscription.fromJson(e)).toList();
}
}

View File

@@ -30,38 +30,28 @@ class Message {
});
factory Message.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'message_id': String messageID,
'sender_user_id': String senderUserID,
'channel_internal_name': String channelInternalName,
'channel_id': String channelID,
'sender_name': String? senderName,
'sender_ip': String senderIP,
'timestamp': String timestamp,
'title': String title,
'content': String? content,
'priority': int priority,
'usr_message_id': String? userMessageID,
'used_key_id': String usedKeyID,
'trimmed': bool trimmed,
} =>
Message(
messageID: messageID,
senderUserID: senderUserID,
channelInternalName: channelInternalName,
channelID: channelID,
senderName: senderName,
senderIP: senderIP,
timestamp: timestamp,
title: title,
content: content,
priority: priority,
userMessageID: userMessageID,
usedKeyID: usedKeyID,
trimmed: trimmed,
),
_ => throw const FormatException('Failed to decode Message.'),
};
return Message(
messageID: json['message_id'],
senderUserID: json['sender_user_id'],
channelInternalName: json['channel_internal_name'],
channelID: json['channel_id'],
senderName: json['sender_name'],
senderIP: json['sender_ip'],
timestamp: json['timestamp'],
title: json['title'],
content: json['content'],
priority: json['priority'],
userMessageID: json['usr_message_id'],
usedKeyID: json['used_key_id'],
trimmed: json['trimmed'],
);
}
static fromPaginatedJsonArray(Map<String, dynamic> data, String keyMessages, String keyToken) {
final npt = data[keyToken] as String;
final messages = (data[keyMessages] as List<dynamic>).map<Message>((e) => Message.fromJson(e)).toList();
return (npt, messages);
}
}

View File

@@ -18,26 +18,14 @@ class Subscription {
});
factory Subscription.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'subscription_id': String subscriptionID,
'subscriber_user_id': String subscriberUserID,
'channel_owner_user_id': String channelOwnerUserID,
'channel_id': String channelID,
'channel_internal_name': String channelInternalName,
'timestamp_created': String timestampCreated,
'confirmed': bool confirmed,
} =>
Subscription(
subscriptionID: subscriptionID,
subscriberUserID: subscriberUserID,
channelOwnerUserID: channelOwnerUserID,
channelID: channelID,
channelInternalName: channelInternalName,
timestampCreated: timestampCreated,
confirmed: confirmed,
),
_ => throw const FormatException('Failed to decode Subscription.'),
};
return Subscription(
subscriptionID: json['subscription_id'],
subscriberUserID: json['subscriber_user_id'],
channelOwnerUserID: json['channel_owner_user_id'],
channelID: json['channel_id'],
channelInternalName: json['channel_internal_name'],
timestampCreated: json['timestamp_created'],
confirmed: json['confirmed'],
);
}
}

View File

@@ -40,48 +40,25 @@ class User {
});
factory User.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'user_id': String userID,
'username': String? username,
'timestamp_created': String timestampCreated,
'timestamp_lastread': String? timestampLastRead,
'timestamp_lastsent': String? timestampLastSent,
'messages_sent': int messagesSent,
'quota_used': int quotaUsed,
'quota_remaining': int quotaRemaining,
'quota_max': int quotaPerDay,
'is_pro': bool isPro,
'default_channel': String defaultChannel,
'max_body_size': int maxBodySize,
'max_title_length': int maxTitleLength,
'default_priority': int defaultPriority,
'max_channel_name_length': int maxChannelNameLength,
'max_channel_description_length': int maxChannelDescriptionLength,
'max_sender_name_length': int maxSenderNameLength,
'max_user_message_id_length': int maxUserMessageIDLength,
} =>
User(
userID: userID,
username: username,
timestampCreated: timestampCreated,
timestampLastRead: timestampLastRead,
timestampLastSent: timestampLastSent,
messagesSent: messagesSent,
quotaUsed: quotaUsed,
quotaRemaining: quotaRemaining,
quotaPerDay: quotaPerDay,
isPro: isPro,
defaultChannel: defaultChannel,
maxBodySize: maxBodySize,
maxTitleLength: maxTitleLength,
defaultPriority: defaultPriority,
maxChannelNameLength: maxChannelNameLength,
maxChannelDescriptionLength: maxChannelDescriptionLength,
maxSenderNameLength: maxSenderNameLength,
maxUserMessageIDLength: maxUserMessageIDLength,
),
_ => throw const FormatException('Failed to decode User.'),
};
return User(
userID: json['user_id'],
username: json['username'],
timestampCreated: json['timestamp_created'],
timestampLastRead: json['timestamp_lastread'],
timestampLastSent: json['timestamp_lastsent'],
messagesSent: json['messages_sent'],
quotaUsed: json['quota_used'],
quotaRemaining: json['quota_remaining'],
quotaPerDay: json['quota_max'],
isPro: json['is_pro'],
defaultChannel: json['default_channel'],
maxBodySize: json['max_body_size'],
maxTitleLength: json['max_title_length'],
defaultPriority: json['default_priority'],
maxChannelNameLength: json['max_channel_name_length'],
maxChannelDescriptionLength: json['max_channel_description_length'],
maxSenderNameLength: json['max_sender_name_length'],
maxUserMessageIDLength: json['max_user_message_id_length'],
);
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
class DebugLogsPage extends StatefulWidget {
@override
_DebugLogsPageState createState() => _DebugLogsPageState();
}
class _DebugLogsPageState extends State<DebugLogsPage> {
@override
Widget build(BuildContext context) {
return Container(/* Add your UI components here */);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/pages/debug/debug_colors.dart';
import 'package:simplecloudnotifier/pages/debug/debug_logs.dart';
import 'package:simplecloudnotifier/pages/debug/debug_persistence.dart';
import 'package:simplecloudnotifier/pages/debug/debug_requests.dart';
@@ -9,13 +10,14 @@ class DebugMainPage extends StatefulWidget {
_DebugMainPageState createState() => _DebugMainPageState();
}
enum DebugMainPageSubPage { colors, requests, persistence }
enum DebugMainPageSubPage { colors, requests, persistence, logs }
class _DebugMainPageState extends State<DebugMainPage> {
final Map<DebugMainPageSubPage, Widget> _subpages = {
DebugMainPageSubPage.colors: DebugColorsPage(),
DebugMainPageSubPage.requests: DebugRequestsPage(),
DebugMainPageSubPage.persistence: DebugPersistencePage(),
DebugMainPageSubPage.logs: DebugLogsPage(),
};
DebugMainPageSubPage _subPage = DebugMainPageSubPage.colors;
@@ -52,6 +54,7 @@ class _DebugMainPageState extends State<DebugMainPage> {
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.colors, label: Text('Theme')),
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.requests, label: Text('Requests')),
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.persistence, label: Text('Persistence')),
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.logs, label: Text('Logs')),
],
selected: <DebugMainPageSubPage>{_subPage},
onSelectionChanged: (Set<DebugMainPageSubPage> v) {

View File

@@ -0,0 +1,87 @@
import 'package:fl_toast/fl_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/state/request_log.dart';
class DebugRequestViewPage extends StatelessWidget {
final SCNRequest request;
DebugRequestViewPage({required this.request});
@override
Widget build(BuildContext context) {
return SCNScaffold(
title: 'Request',
showSearch: false,
showDebug: false,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: [
...buildRow(context, "Name", request.name),
...buildRow(context, "Timestamp (Start)", request.timestampStart.toString()),
...buildRow(context, "Timestamp (End)", request.timestampEnd.toString()),
...buildRow(context, "Duration", request.timestampEnd.difference(request.timestampStart).toString()),
Divider(),
...buildRow(context, "Method", request.method),
...buildRow(context, "URL", request.url),
if (request.requestHeaders.isNotEmpty) ...buildRow(context, "Request->Headers", request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
if (request.requestBody != '') ...buildRow(context, "Request->Body", request.requestBody),
Divider(),
if (request.responseStatusCode != 0) ...buildRow(context, "Response->Statuscode", request.responseStatusCode.toString()),
if (request.responseBody != '') ...buildRow(context, "Reponse->Body", request.responseBody),
if (request.responseHeaders.isNotEmpty) ...buildRow(context, "Reponse->Headers", request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
Divider(),
if (request.error != '') ...buildRow(context, "Error", request.error),
if (request.stackTrace != '') ...buildRow(context, "Stacktrace", request.stackTrace),
],
),
),
),
);
}
List<Widget> buildRow(BuildContext context, String title, String value) {
return [
Padding(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8.0),
child: Row(
children: [
Expanded(
child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
),
IconButton(
icon: FaIcon(
FontAwesomeIcons.copy,
),
iconSize: 14,
padding: EdgeInsets.fromLTRB(0, 0, 4, 0),
constraints: BoxConstraints(),
onPressed: () {
Clipboard.setData(new ClipboardData(text: value));
showPlatformToast(child: Text('Copied to clipboard'), context: ToastProvider.context);
},
),
],
),
),
Card.filled(
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
color: request.type == 'SUCCESS' ? null : Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0),
child: SelectableText(
value,
minLines: 1,
maxLines: 10,
),
),
),
];
}
}

View File

@@ -1,4 +1,8 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:intl/intl.dart';
import 'package:simplecloudnotifier/pages/debug/debug_request_view.dart';
import 'package:simplecloudnotifier/state/request_log.dart';
class DebugRequestsPage extends StatefulWidget {
@override
@@ -6,8 +10,82 @@ class DebugRequestsPage extends StatefulWidget {
}
class _DebugRequestsPageState extends State<DebugRequestsPage> {
Box<SCNRequest> requestsBox = Hive.box<SCNRequest>('scn-requests');
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
@override
Widget build(BuildContext context) {
return Container(/* Add your UI components here */);
return Container(
child: ValueListenableBuilder(
valueListenable: requestsBox.listenable(),
builder: (context, Box<SCNRequest> box, _) {
return ListView.builder(
itemCount: requestsBox.length,
itemBuilder: (context, listIndex) {
final req = requestsBox.getAt(requestsBox.length - listIndex - 1)!;
if (req.type == 'SUCCESS') {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0),
child: GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => DebugRequestViewPage(request: req))),
child: ListTile(
title: Row(
children: [
SizedBox(
width: 120,
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
),
Expanded(
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
),
SizedBox(width: 2),
Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)),
],
),
subtitle: Text(req.type),
),
),
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0),
child: GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => DebugRequestViewPage(request: req))),
child: ListTile(
tileColor: Theme.of(context).colorScheme.errorContainer,
textColor: Theme.of(context).colorScheme.onErrorContainer,
title: Row(
children: [
SizedBox(
width: 120,
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
),
Expanded(
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
),
SizedBox(width: 2),
Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(req.type),
Text(
req.error,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
)),
),
);
}
},
);
},
),
);
}
}

View File

@@ -1 +1,29 @@
import 'package:hive_flutter/hive_flutter.dart';
part 'application_log.g.dart';
class ApplicationLog {}
enum SCNLogLevel { debug, info, warning, error, fatal }
@HiveType(typeId: 101)
class SCNLog extends HiveObject {
@HiveField(0)
final DateTime timestamp;
@HiveField(1)
final SCNLogLevel level;
@HiveField(2)
final String message;
@HiveField(3)
final String additional;
@HiveField(4)
final String trace;
SCNLog(
this.timestamp,
this.level,
this.message,
this.additional,
this.trace,
);
}

View File

@@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'application_log.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SCNLogAdapter extends TypeAdapter<SCNLog> {
@override
final int typeId = 101;
@override
SCNLog read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return SCNLog(
fields[0] as DateTime,
fields[1] as SCNLogLevel,
fields[2] as String,
fields[3] as String,
fields[4] as String,
);
}
@override
void write(BinaryWriter writer, SCNLog obj) {
writer
..writeByte(5)
..writeByte(0)
..write(obj.timestamp)
..writeByte(1)
..write(obj.level)
..writeByte(2)
..write(obj.message)
..writeByte(3)
..write(obj.additional)
..writeByte(4)
..write(obj.trace);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SCNLogAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -1,46 +0,0 @@
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'dart:io';
import 'package:path/path.dart' as path;
class SCNDatabase {
static SCNDatabase? instance = null;
final Database _db;
SCNDatabase._(this._db) {}
static create() async {
var docPath = await getApplicationDocumentsDirectory();
var dbpath = path.join(docPath.absolute.path, 'scn.db');
if (Platform.isWindows || Platform.isLinux) {
sqfliteFfiInit();
}
var db = await databaseFactoryFfi.openDatabase(dbpath,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
initDatabase(db);
},
onUpgrade: (db, oldVersion, newVersion) async {
upgradeDatabase(db, oldVersion, newVersion);
},
));
return instance = SCNDatabase._(db);
}
static void initDatabase(Database db) async {
await db.execute('CREATE TABLE requests (id INTEGER PRIMARY KEY, timestamp DATETIME, name TEXT, url TEXT, response_code INTEGER, response TEXT, status TEXT)');
await db.execute('CREATE TABLE logs (id INTEGER PRIMARY KEY, timestamp DATETIME, level TEXT, text TEXT, additional TEXT)');
await db.execute('CREATE TABLE messages (message_id INTEGER PRIMARY KEY, receive_timestamp DATETIME, channel_id TEXT, timestamp TEXT, data JSON)');
}
static void upgradeDatabase(Database db, int oldVersion, int newVersion) {
// ...
}
}

View File

@@ -0,0 +1,31 @@
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
class Globals {
static final Globals _singleton = Globals._internal();
factory Globals() {
return _singleton;
}
Globals._internal();
String appName = '';
String packageName = '';
String version = '';
String buildNumber = '';
String platform = '';
String hostname = '';
init() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
this.appName = packageInfo.appName;
this.packageName = packageInfo.packageName;
this.version = packageInfo.version;
this.buildNumber = packageInfo.buildNumber;
this.platform = Platform.operatingSystem;
this.hostname = Platform.localHostname;
}
}

View File

@@ -1 +1,144 @@
import 'package:hive/hive.dart';
import 'package:simplecloudnotifier/models/api_error.dart';
part 'request_log.g.dart';
class RequestLog {
static void addRequestException(String name, DateTime tStart, String method, Uri uri, String reqbody, Map<String, String> reqheaders, dynamic e, StackTrace trace) {
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
timestampStart: tStart,
timestampEnd: DateTime.now(),
name: name,
method: method,
url: uri.toString(),
requestHeaders: reqheaders,
requestBody: reqbody,
responseStatusCode: 0,
responseHeaders: {},
responseBody: '',
type: 'EXCEPTION',
error: (e is Exception) ? e.toString() : '$e',
stackTrace: trace.toString(),
));
}
static void addRequestAPIError(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders, APIError apierr) {
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
timestampStart: t0,
timestampEnd: DateTime.now(),
name: name,
method: method,
url: uri.toString(),
requestHeaders: reqheaders,
requestBody: reqbody,
responseStatusCode: responseStatusCode,
responseHeaders: responseHeaders,
responseBody: responseBody,
type: 'API_ERROR',
error: apierr.message,
stackTrace: '',
));
}
static void addRequestErrorStatuscode(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders) {
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
timestampStart: t0,
timestampEnd: DateTime.now(),
name: name,
method: method,
url: uri.toString(),
requestHeaders: reqheaders,
requestBody: reqbody,
responseStatusCode: responseStatusCode,
responseHeaders: responseHeaders,
responseBody: responseBody,
type: 'ERROR_STATUSCODE',
error: 'API request failed with status code $responseStatusCode',
stackTrace: '',
));
}
static void addRequestSuccess(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders) {
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
timestampStart: t0,
timestampEnd: DateTime.now(),
name: name,
method: method,
url: uri.toString(),
requestHeaders: reqheaders,
requestBody: reqbody,
responseStatusCode: responseStatusCode,
responseHeaders: responseHeaders,
responseBody: responseBody,
type: 'SUCCESS',
error: '',
stackTrace: '',
));
}
static void addRequestDecodeError(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders, Object exc, StackTrace trace) {
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
timestampStart: t0,
timestampEnd: DateTime.now(),
name: name,
method: method,
url: uri.toString(),
requestHeaders: reqheaders,
requestBody: reqbody,
responseStatusCode: responseStatusCode,
responseHeaders: responseHeaders,
responseBody: responseBody,
type: 'DECODE_ERROR',
error: (exc is Exception) ? exc.toString() : '$exc',
stackTrace: trace.toString(),
));
}
}
@HiveType(typeId: 100)
class SCNRequest extends HiveObject {
@HiveField(0)
final DateTime timestampStart;
@HiveField(1)
final DateTime timestampEnd;
@HiveField(2)
final String name;
@HiveField(3)
final String type;
@HiveField(4)
final String error;
@HiveField(5)
final String stackTrace;
@HiveField(6)
final String method;
@HiveField(7)
final String url;
@HiveField(8)
final Map<String, String> requestHeaders;
@HiveField(12)
final String requestBody;
@HiveField(9)
final int responseStatusCode;
@HiveField(10)
final Map<String, String> responseHeaders;
@HiveField(11)
final String responseBody;
SCNRequest({
required this.timestampStart,
required this.timestampEnd,
required this.name,
required this.method,
required this.url,
required this.requestHeaders,
required this.requestBody,
required this.responseStatusCode,
required this.responseHeaders,
required this.responseBody,
required this.type,
required this.error,
required this.stackTrace,
});
}

View File

@@ -0,0 +1,77 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'request_log.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SCNRequestAdapter extends TypeAdapter<SCNRequest> {
@override
final int typeId = 100;
@override
SCNRequest read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return SCNRequest(
timestampStart: fields[0] as DateTime,
timestampEnd: fields[1] as DateTime,
name: fields[2] as String,
method: fields[6] as String,
url: fields[7] as String,
requestHeaders: (fields[8] as Map).cast<String, String>(),
requestBody: fields[12] as String,
responseStatusCode: fields[9] as int,
responseHeaders: (fields[10] as Map).cast<String, String>(),
responseBody: fields[11] as String,
type: fields[3] as String,
error: fields[4] as String,
stackTrace: fields[5] as String,
);
}
@override
void write(BinaryWriter writer, SCNRequest obj) {
writer
..writeByte(13)
..writeByte(0)
..write(obj.timestampStart)
..writeByte(1)
..write(obj.timestampEnd)
..writeByte(2)
..write(obj.name)
..writeByte(3)
..write(obj.type)
..writeByte(4)
..write(obj.error)
..writeByte(5)
..write(obj.stackTrace)
..writeByte(6)
..write(obj.method)
..writeByte(7)
..write(obj.url)
..writeByte(8)
..write(obj.requestHeaders)
..writeByte(12)
..write(obj.requestBody)
..writeByte(9)
..write(obj.responseStatusCode)
..writeByte(10)
..write(obj.responseHeaders)
..writeByte(11)
..write(obj.responseBody);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SCNRequestAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -71,7 +71,7 @@ class UserAccount extends ChangeNotifier {
throw Exception('Not authenticated');
}
final user = await APIClient.getUser(_auth!.userId, _auth!.token);
final user = await APIClient.getUser(_auth!, _auth!.userId);
setUser(user);