@@ -3,10 +3,14 @@ import 'dart:convert';
3
3
import 'package:flutter/material.dart' ;
4
4
import 'package:logging/logging.dart' ;
5
5
6
+ import 'package:chopper/chopper.dart' ;
6
7
import 'package:flutter_local_notifications/flutter_local_notifications.dart' ;
7
8
import 'package:notifications_listener_service/notifications_listener_service.dart' ;
9
+ import 'package:timezone/data/latest.dart' as tz;
8
10
9
11
import 'package:waterflyiii/app.dart' ;
12
+ import 'package:waterflyiii/auth.dart' ;
13
+ import 'package:waterflyiii/generated/swagger_fireflyiii_api/firefly_iii.swagger.dart' ;
10
14
import 'package:waterflyiii/pages/transaction.dart' ;
11
15
import 'package:waterflyiii/settings.dart' ;
12
16
@@ -70,17 +74,19 @@ void nlCallback() async {
70
74
log.finest (() => "nlCallback()" );
71
75
NotificationServicePlugin .instance
72
76
.executeNotificationListener ((NotificationEvent ? evt) async {
73
- if (evt? .packageName? . startsWith ( "com.dreautall.waterflyiii" ) ?? false ) {
77
+ if (evt == null || evt .packageName == null ) {
74
78
return ;
75
79
}
76
- if (evt? .state == NotificationState .remove) {
80
+ if (evt.packageName? .startsWith ("com.dreautall.waterflyiii" ) ?? false ) {
81
+ return ;
82
+ }
83
+ if (evt.state == NotificationState .remove) {
77
84
return ;
78
85
}
79
86
80
- final Iterable <RegExpMatch > matches =
81
- rFindMoney.allMatches (evt? .text ?? "" );
87
+ final Iterable <RegExpMatch > matches = rFindMoney.allMatches (evt.text ?? "" );
82
88
if (matches.isEmpty) {
83
- log.finer (() => "nlCallback(${evt ? .packageName }): no money found" );
89
+ log.finer (() => "nlCallback(${evt .packageName }): no money found" );
84
90
return ;
85
91
}
86
92
@@ -93,40 +99,142 @@ void nlCallback() async {
93
99
}
94
100
}
95
101
if (! validMatch) {
96
- log.finer (() =>
97
- "nlCallback(${evt ? .packageName }): no money with currency found" );
102
+ log.finer (
103
+ () => "nlCallback(${evt .packageName }): no money with currency found" );
98
104
return ;
99
105
}
100
106
101
- SettingsProvider (). notificationAddKnownApp (evt ? .packageName ?? "" );
107
+ final SettingsProvider settings = SettingsProvider ( );
102
108
103
- if (! (await SettingsProvider ().notificationUsedApps (forceReload: true ))
104
- .contains (evt? .packageName ?? "" )) {
105
- log.finer (() => "nlCallback(${evt ?.packageName }): app not used" );
109
+ settings.notificationAddKnownApp (evt.packageName! );
110
+
111
+ if (! (await settings.notificationUsedApps ()).contains (evt.packageName)) {
112
+ log.finer (() => "nlCallback(${evt .packageName }): app not used" );
106
113
return ;
107
114
}
108
115
109
- FlutterLocalNotificationsPlugin ().show (
110
- DateTime .now ().millisecondsSinceEpoch ~ / 1000 ,
111
- "Create Transaction?" ,
112
- "Click to create a transaction based on the notification ${evt ?.title }" ,
113
- const NotificationDetails (
114
- android: AndroidNotificationDetails (
115
- 'extract_transaction' ,
116
- 'Create Transaction from Notification' ,
117
- channelDescription:
118
- 'Notification asking to create a transaction from another Notification.' ,
119
- importance: Importance .low, // Android 8.0 and higher
120
- priority: Priority .low, // Android 7.1 and lower
116
+ final NotificationAppSettings appSettings =
117
+ await settings.notificationGetAppSettings (evt.packageName! );
118
+ bool showNotification = true ;
119
+
120
+ if (appSettings.autoAdd) {
121
+ tz.initializeTimeZones ();
122
+ log.finer (() =>
123
+ "nlCallback(${evt .packageName }): trying to auto-add transaction" );
124
+ try {
125
+ final FireflyService ffService = FireflyService ();
126
+ if (! await ffService.signInFromStorage ()) {
127
+ throw UnauthenticatedResponse ;
128
+ }
129
+ final FireflyIii api = ffService.api;
130
+ final CurrencyRead localCurrency = ffService.defaultCurrency;
131
+ late CurrencyRead ? currency;
132
+ late double amount;
133
+
134
+ (currency, amount) =
135
+ await parseNotificationText (api, evt.text! , localCurrency);
136
+ // Fallback solution
137
+ currency ?? = localCurrency;
138
+
139
+ // Set date
140
+ DateTime date = ffService.tzHandler
141
+ .notificationTXTime (
142
+ DateTime .tryParse (evt.postTime ?? "" ) ?? DateTime .now ())
143
+ .toLocal ();
144
+ String note = "" ;
145
+ if (appSettings.autoAdd) {
146
+ note = evt.text ?? "" ;
147
+ }
148
+
149
+ // Check currency
150
+ if (currency != localCurrency) {
151
+ throw Exception ("Can't auto-add TX with foreign currency" );
152
+ }
153
+
154
+ // Check account
155
+ if (appSettings.defaultAccountId == null ) {
156
+ throw Exception ("Can't auto-add TX with no default account ID" );
157
+ }
158
+
159
+ final TransactionStore newTx = TransactionStore (
160
+ groupTitle: null ,
161
+ transactions: < TransactionSplitStore > [
162
+ TransactionSplitStore (
163
+ type: TransactionTypeProperty .withdrawal,
164
+ date: date,
165
+ amount: amount.toString (),
166
+ description: evt.title! ,
167
+ // destinationId
168
+ // destinationName
169
+ notes: note,
170
+ order: 0 ,
171
+ sourceId: appSettings.defaultAccountId,
172
+ )
173
+ ],
174
+ applyRules: true ,
175
+ fireWebhooks: true ,
176
+ errorIfDuplicateHash: true ,
177
+ );
178
+ final Response <TransactionSingle > resp =
179
+ await api.v1TransactionsPost (body: newTx);
180
+ if (! resp.isSuccessful || resp.body == null ) {
181
+ try {
182
+ ValidationErrorResponse valError = ValidationErrorResponse .fromJson (
183
+ json.decode (resp.error.toString ()),
184
+ );
185
+ throw Exception (valError.message);
186
+ } catch (_) {
187
+ throw Exception ("unknown" );
188
+ }
189
+ }
190
+
191
+ FlutterLocalNotificationsPlugin ().show (
192
+ DateTime .now ().millisecondsSinceEpoch ~ / 1000 ,
193
+ "Transaction created" ,
194
+ "Transaction created based on notification ${evt .title }" ,
195
+ const NotificationDetails (
196
+ android: AndroidNotificationDetails (
197
+ 'extract_transaction_created' ,
198
+ 'Transaction from Notification Created' ,
199
+ channelDescription:
200
+ 'Notification that a Transaction has been created from another Notification.' ,
201
+ importance: Importance .low, // Android 8.0 and higher
202
+ priority: Priority .low, // Android 7.1 and lower
203
+ ),
204
+ ),
205
+ payload: "" ,
206
+ );
207
+
208
+ showNotification = false ;
209
+ } catch (e, stackTrace) {
210
+ log.severe ("Error while auto-adding transaction" , e, stackTrace);
211
+ showNotification = true ;
212
+ }
213
+ }
214
+
215
+ if (showNotification) {
216
+ FlutterLocalNotificationsPlugin ().show (
217
+ DateTime .now ().millisecondsSinceEpoch ~ / 1000 ,
218
+ "Create Transaction?" ,
219
+ "Click to create a transaction based on the notification ${evt .title }" ,
220
+ const NotificationDetails (
221
+ android: AndroidNotificationDetails (
222
+ 'extract_transaction' ,
223
+ 'Create Transaction from Notification' ,
224
+ channelDescription:
225
+ 'Notification asking to create a transaction from another Notification.' ,
226
+ importance: Importance .low, // Android 8.0 and higher
227
+ priority: Priority .low, // Android 7.1 and lower
228
+ ),
121
229
),
122
- ),
123
- payload : jsonEncode ( NotificationTransaction (
124
- evt? .packageName ?? "" ,
125
- evt? .title ?? "" ,
126
- evt? .text ?? "" ,
127
- DateTime . tryParse (evt ? .postTime ?? "" ) ?? DateTime . now ( ),
128
- )),
129
- );
230
+ payload : jsonEncode ( NotificationTransaction (
231
+ evt.packageName ?? "" ,
232
+ evt.title ?? "" ,
233
+ evt.text ?? "" ,
234
+ DateTime . tryParse ( evt.postTime ?? "" ) ?? DateTime . now () ,
235
+ ) ),
236
+ );
237
+ }
130
238
});
131
239
}
132
240
@@ -150,3 +258,88 @@ Future<void> nlNotificationTap(
150
258
),
151
259
);
152
260
}
261
+
262
+ Future <(CurrencyRead ?, double )> parseNotificationText (
263
+ FireflyIii api, String notificationBody, CurrencyRead localCurrency) async {
264
+ CurrencyRead ? currency;
265
+ double amount = 0 ;
266
+ // Try to extract some money
267
+ final Iterable <RegExpMatch > matches = rFindMoney.allMatches (notificationBody);
268
+ if (matches.isNotEmpty) {
269
+ RegExpMatch ? validMatch;
270
+ for (RegExpMatch match in matches) {
271
+ if ((match.namedGroup ("postCurrency" )? .isNotEmpty ?? false ) ||
272
+ (match.namedGroup ("preCurrency" )? .isNotEmpty ?? false )) {
273
+ validMatch = match;
274
+ break ;
275
+ }
276
+ }
277
+ if (validMatch != null ) {
278
+ // extract currency
279
+ String currencyStr = validMatch.namedGroup ("preCurrency" ) ?? "" ;
280
+ String currencyStrAlt = validMatch.namedGroup ("postCurrency" ) ?? "" ;
281
+ if (currencyStr.isEmpty) {
282
+ currencyStr = currencyStrAlt;
283
+ }
284
+ if (currencyStr.isEmpty) {
285
+ log.warning ("no currency found" );
286
+ }
287
+ if (localCurrency.attributes.code == currencyStr ||
288
+ localCurrency.attributes.symbol == currencyStr ||
289
+ localCurrency.attributes.code == currencyStrAlt ||
290
+ localCurrency.attributes.symbol == currencyStrAlt) {
291
+ } else {
292
+ final Response <CurrencyArray > response = await api.v1CurrenciesGet ();
293
+ if (! response.isSuccessful || response.body == null ) {
294
+ log.warning ("api currency fetch failed" );
295
+ } else {
296
+ for (CurrencyRead cur in response.body! .data) {
297
+ if (cur.attributes.code == currencyStr ||
298
+ cur.attributes.symbol == currencyStr ||
299
+ cur.attributes.code == currencyStrAlt ||
300
+ cur.attributes.symbol == currencyStrAlt) {
301
+ currency = cur;
302
+ break ;
303
+ }
304
+ }
305
+ }
306
+ }
307
+ // extract amount
308
+ // Check if string has a decimal separator
309
+ final String amountStr =
310
+ (validMatch.namedGroup ("amount" ) ?? "" ).replaceAll (" " , "" );
311
+ final int decimalSepPos = amountStr.length >= 3 &&
312
+ (amountStr[amountStr.length - 3 ] == "." ||
313
+ amountStr[amountStr.length - 3 ] == "," )
314
+ ? amountStr.length - 3
315
+ : amountStr.length - 2 ;
316
+ final String decimalSep =
317
+ amountStr.length >= decimalSepPos && decimalSepPos > 0
318
+ ? amountStr[decimalSepPos]
319
+ : "" ;
320
+ if (decimalSep == "," || decimalSep == "." ) {
321
+ final double wholes = double .tryParse (amountStr
322
+ .substring (0 , decimalSepPos)
323
+ .replaceAll ("," , "" )
324
+ .replaceAll ("." , "" )) ??
325
+ 0 ;
326
+ final String decStr = amountStr
327
+ .substring (decimalSepPos + 1 )
328
+ .replaceAll ("," , "" )
329
+ .replaceAll ("." , "" );
330
+ final double dec = double .tryParse (decStr) ?? 0 ;
331
+ amount = decStr.length == 1 ? wholes + dec / 10 : wholes + dec / 100 ;
332
+ } else {
333
+ amount = double .tryParse (
334
+ amountStr.replaceAll ("," , "" ).replaceAll ("." , "" )) ??
335
+ 0 ;
336
+ }
337
+ } else {
338
+ log.info ("no currency was found" );
339
+ }
340
+ } else {
341
+ log.warning ("regex did not match" );
342
+ }
343
+
344
+ return (currency, amount);
345
+ }
0 commit comments