Skip to content

Commit ab251c1

Browse files
author
Mahdi
committed
Add Kerio balance and show notification if low
1 parent c2074ab commit ab251c1

4 files changed

Lines changed: 220 additions & 12 deletions

File tree

lib/bloc.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import 'package:live_event/live_event.dart';
1717
import 'package:rxdart/rxdart.dart';
1818
import 'package:win_toast/win_toast.dart';
1919

20+
import 'utils/kerio.dart';
21+
2022
class AppBloc with AppSystemTray {
2123
final _latLng = StreamController<LatLng>();
2224
final _ipLookupResult = BehaviorSubject();
@@ -58,6 +60,7 @@ class AppBloc with AppSystemTray {
5860
initSystemTray();
5961
_pingGoogle();
6062
_runIpCheckInfinitely();
63+
_runKerioCheckInfinitely();
6164
_subscribeConnectivityChange();
6265
_initializeLeakChecklist();
6366
}
@@ -167,6 +170,13 @@ class AppBloc with AppSystemTray {
167170
}
168171
}
169172

173+
void _runKerioCheckInfinitely() async {
174+
while (true) {
175+
_checkKerioBalance();
176+
await Future.delayed(const Duration(seconds: 60));
177+
}
178+
}
179+
170180
void _pingGoogle() async {
171181
try {
172182
_isPingingGoogle = true;
@@ -233,6 +243,28 @@ class AppBloc with AppSystemTray {
233243
}
234244
}
235245

246+
void _checkKerioBalance() async {
247+
final (total, remaining) = await KerioUtils.getAccountBalance();
248+
var lowBalanceToastCount = await AppSharedPreferences.kerioLowBalanceToastCount;
249+
var lastToastDate = await AppSharedPreferences.kerioLowBalanceToastDate;
250+
251+
final now = DateTime.now();
252+
final today = DateTime(now.year, now.month, now.day);
253+
254+
if (lastToastDate == null || DateTime.parse(lastToastDate).isBefore(today)) {
255+
// New day, reset count
256+
lowBalanceToastCount = 0;
257+
await AppSharedPreferences.setKerioLowBalanceToastCount(0);
258+
await AppSharedPreferences.setKerioLowBalanceToastDate(today.toIso8601String());
259+
}
260+
261+
if (remaining < 1073741824 * 10 && lowBalanceToastCount < 2) {
262+
WinToast.instance().showToast(type: ToastType.text01, title: 'Less than 1 GB is left in your kerio account!');
263+
await AppSharedPreferences.setKerioLowBalanceToastCount(lowBalanceToastCount + 1);
264+
await AppSharedPreferences.setKerioLowBalanceToastDate(today.toIso8601String());
265+
}
266+
}
267+
236268
void _checkNetworkConnectivity() async {
237269
final result = await (Connectivity().checkConnectivity());
238270
if (result == ConnectivityResult.none) {

lib/data/shared_preferences.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ class AppSharedPreferences {
3333
(await _preference).setBool(_keyKerioAutoLogin, value);
3434
}
3535

36+
static Future<int> get kerioLowBalanceToastCount async {
37+
return (await _preference).getInt(_keyKerioLowBalanceToastCount) ?? 0;
38+
}
39+
40+
static Future<void> setKerioLowBalanceToastCount(int value) async {
41+
(await _preference).setInt(_keyKerioLowBalanceToastCount, value);
42+
}
43+
44+
static Future<String?> get kerioLowBalanceToastDate async {
45+
return (await _preference).getString(_keyKerioLowBalanceToastDate);
46+
}
47+
48+
static Future<void> setKerioLowBalanceToastDate(String value) async {
49+
(await _preference).setString(_keyKerioLowBalanceToastDate, value);
50+
}
51+
3652
static Future<bool> get showLeakInSysTray async {
3753
return (await _preference).getBool(_keyShowLeakInSysTray) ?? true;
3854
}
@@ -82,4 +98,6 @@ class AppSharedPreferences {
8298
static const _keyKerioUsername = 'kerioUsername';
8399
static const _keyKerioPassword = 'kerioPassword';
84100
static const _keyKerioAutoLogin = 'kerioAutoLogin';
101+
static const _keyKerioLowBalanceToastCount = 'kerioLowBalanceToastCount';
102+
static const _keyKerioLowBalanceToastDate = 'kerioLowBalanceToastDate';
85103
}

lib/utils/kerio.dart

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import 'dart:convert';
2+
3+
import 'package:http/http.dart' as http;
4+
5+
import '../data/shared_preferences.dart';
6+
7+
class KerioUtils {
8+
static Future<(int, int)> getAccountBalance() async {
9+
final ip = await AppSharedPreferences.kerioIP;
10+
final username = await AppSharedPreferences.kerioUsername;
11+
final password = await AppSharedPreferences.kerioPassword;
12+
13+
// Step 1: Login and extract cookie
14+
final loginUrl = 'http://$ip/internal/dologin.php';
15+
final loginResponse = await http.post(
16+
Uri.parse(loginUrl),
17+
body: {
18+
'kerio_username': username,
19+
'kerio_password': password,
20+
},
21+
);
22+
23+
final cookieHeader = loginResponse.headers['set-cookie'];
24+
if (cookieHeader == null || !cookieHeader.contains('TOKEN_CONTROL_WEBIFACE=')) {
25+
throw Exception('Login failed or TOKEN_CONTROL_WEBIFACE not found.');
26+
}
27+
28+
final cookies = cookieHeader.split(',');
29+
final tokenCookie = cookies
30+
.map((c) => c.trim())
31+
.firstWhere(
32+
(c) => c.startsWith('TOKEN_CONTROL_WEBIFACE='),
33+
orElse: () => throw Exception('TOKEN_CONTROL_WEBIFACE not found in cookies'),
34+
);
35+
36+
final token = tokenCookie.split('=')[1].split(';')[0];
37+
38+
final rawCookies = cookieHeader.split(',');
39+
final cookieMap = <String, String>{};
40+
for (var cookie in rawCookies) {
41+
final parts = cookie.split(';')[0].trim(); // Take only key=value part
42+
final kv = parts.split('=');
43+
if (kv.length == 2) {
44+
cookieMap[kv[0]] = kv[1];
45+
}
46+
}
47+
final cookieHeaderValue = cookieMap.entries.map((e) => '${e.key}=${e.value}').join('; ');
48+
49+
// Step 2: Prepare headers
50+
final balanceUrl = 'http://$ip/lib/api/jsonrpc/';
51+
final headers = {
52+
'Accept': '*/*',
53+
'Accept-Language': 'en-US,en;q=0.9,fa;q=0.8',
54+
'Connection': 'keep-alive',
55+
'Content-Type': 'application/json',
56+
'Origin': 'http://$ip',
57+
'Referer': 'http://$ip/',
58+
'User-Agent':
59+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
60+
'X-Requested-With': 'XMLHttpRequest',
61+
'X-Token': token,
62+
'Cookie': cookieHeaderValue
63+
};
64+
65+
// Step 3: Body
66+
final body = jsonEncode({
67+
"jsonrpc": "2.0",
68+
"id": 1,
69+
"method": "Batch.run",
70+
"params": {
71+
"commandList": [
72+
{"method": "MyAccount.get"},
73+
{"method": "MyAccount.getRasIntefaces"}
74+
]
75+
}
76+
});
77+
78+
// Step 4: Send request
79+
final balanceResponse = await http.post(
80+
Uri.parse(balanceUrl),
81+
headers: headers,
82+
body: body,
83+
);
84+
85+
if (balanceResponse.statusCode != 200) {
86+
throw Exception('Failed to fetch balance data');
87+
}
88+
89+
final data = jsonDecode(balanceResponse.body);
90+
final quota = data['result'][0]['result']['quota']['month'];
91+
92+
final total = int.parse(quota['value']);
93+
final down = int.parse(quota['down']);
94+
final up = int.parse(quota['up']);
95+
final remaining = total - (down + up);
96+
97+
return (total, remaining);
98+
}
99+
100+
static String formatBytes(int bytes) {
101+
const kb = 1024;
102+
const mb = kb * 1024;
103+
const gb = mb * 1024;
104+
105+
if (bytes >= gb) {
106+
return '${(bytes / gb).toStringAsFixed(2)} GB';
107+
} else if (bytes >= mb) {
108+
return '${(bytes / mb).toStringAsFixed(2)} MB';
109+
} else if (bytes >= kb) {
110+
return '${(bytes / kb).toStringAsFixed(2)} KB';
111+
} else {
112+
return '$bytes B';
113+
}
114+
}
115+
}

lib/views/kerio_login.dart

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:http/http.dart' as http;
33
import 'package:ir_net/data/shared_preferences.dart';
4+
import 'package:ir_net/utils/kerio.dart';
45
import 'package:url_launcher/url_launcher.dart';
56

67
class KerioLoginView extends StatefulWidget {
@@ -89,26 +90,54 @@ class _KerioLoginViewState extends State<KerioLoginView> {
8990
child: Column(
9091
crossAxisAlignment: CrossAxisAlignment.center,
9192
children: [
92-
ipInput(),
93+
ipAndBalanceRow(),
9394
const SizedBox(height: 4),
94-
Row(
95-
children: [
96-
Expanded(
97-
child: username(),
98-
),
99-
const SizedBox(width: 4),
100-
Expanded(
101-
child: password(),
102-
),
103-
],
104-
),
95+
credentialsRow(),
10596
const SizedBox(height: 8),
10697
loginRow()
10798
],
10899
),
109100
);
110101
}
111102

103+
Widget ipAndBalanceRow() {
104+
return Row(
105+
children: [
106+
Expanded(
107+
child: ipInput(),
108+
),
109+
const SizedBox(width: 4),
110+
Expanded(
111+
child: balance(),
112+
),
113+
],
114+
);
115+
}
116+
117+
Widget balance() {
118+
return FutureBuilder(
119+
future: KerioUtils.getAccountBalance(),
120+
builder: (context, snapshot) {
121+
var (total, remaining) = snapshot.data ?? (0, 0);
122+
var totalFormatted = total == 0 ? '--' : KerioUtils.formatBytes(total);
123+
var remainingFormatted = remaining == 0 ? '--' : KerioUtils.formatBytes(remaining);
124+
return Container(
125+
padding: const EdgeInsets.only(left: 16),
126+
child: Column(
127+
crossAxisAlignment: CrossAxisAlignment.start,
128+
children: [
129+
Text(
130+
'Remaining = $remainingFormatted',
131+
style: TextStyle(color: remaining < 1073741824 ? Colors.red : Colors.black),
132+
),
133+
Text('Total = $totalFormatted'),
134+
],
135+
),
136+
);
137+
},
138+
);
139+
}
140+
112141
Widget ipInput() {
113142
return TextField(
114143
controller: _ipController,
@@ -138,6 +167,20 @@ class _KerioLoginViewState extends State<KerioLoginView> {
138167
);
139168
}
140169

170+
Widget credentialsRow() {
171+
return Row(
172+
children: [
173+
Expanded(
174+
child: username(),
175+
),
176+
const SizedBox(width: 4),
177+
Expanded(
178+
child: password(),
179+
),
180+
],
181+
);
182+
}
183+
141184
Widget username() {
142185
return TextField(
143186
controller: _usernameController,

0 commit comments

Comments
 (0)