remoteRF-server-testing 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. remoteRF_server/__init__.py +0 -0
  2. remoteRF_server/common/__init__.py +0 -0
  3. remoteRF_server/common/grpc/__init__.py +1 -0
  4. remoteRF_server/common/grpc/grpc_host_pb2.py +63 -0
  5. remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +97 -0
  6. remoteRF_server/common/grpc/grpc_pb2.py +59 -0
  7. remoteRF_server/common/grpc/grpc_pb2_grpc.py +97 -0
  8. remoteRF_server/common/idl/__init__.py +1 -0
  9. remoteRF_server/common/idl/device_schema.py +39 -0
  10. remoteRF_server/common/idl/pluto_schema.py +174 -0
  11. remoteRF_server/common/idl/schema.py +358 -0
  12. remoteRF_server/common/utils/__init__.py +6 -0
  13. remoteRF_server/common/utils/ansi_codes.py +120 -0
  14. remoteRF_server/common/utils/api_token.py +21 -0
  15. remoteRF_server/common/utils/db_connection.py +35 -0
  16. remoteRF_server/common/utils/db_location.py +24 -0
  17. remoteRF_server/common/utils/list_string.py +5 -0
  18. remoteRF_server/common/utils/process_arg.py +80 -0
  19. remoteRF_server/drivers/__init__.py +0 -0
  20. remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  21. remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +105 -0
  22. remoteRF_server/host/__init__.py +0 -0
  23. remoteRF_server/host/host_auth_token.py +292 -0
  24. remoteRF_server/host/host_directory_store.py +142 -0
  25. remoteRF_server/host/host_tunnel_server.py +1388 -0
  26. remoteRF_server/server/__init__.py +0 -0
  27. remoteRF_server/server/acc_perms.py +317 -0
  28. remoteRF_server/server/cert_provider.py +184 -0
  29. remoteRF_server/server/device_manager.py +688 -0
  30. remoteRF_server/server/grpc_server.py +1023 -0
  31. remoteRF_server/server/reservation.py +811 -0
  32. remoteRF_server/server/rpc_manager.py +104 -0
  33. remoteRF_server/server/user_group_cli.py +723 -0
  34. remoteRF_server/server/user_group_handler.py +1120 -0
  35. remoteRF_server/serverrf_cli.py +1377 -0
  36. remoteRF_server/tools/__init__.py +191 -0
  37. remoteRF_server/tools/gen_certs.py +274 -0
  38. remoteRF_server/tools/gist_status.py +139 -0
  39. remoteRF_server/tools/gist_status_testing.py +67 -0
  40. remoterf_server_testing-0.0.0.dist-info/METADATA +612 -0
  41. remoterf_server_testing-0.0.0.dist-info/RECORD +44 -0
  42. remoterf_server_testing-0.0.0.dist-info/WHEEL +5 -0
  43. remoterf_server_testing-0.0.0.dist-info/entry_points.txt +2 -0
  44. remoterf_server_testing-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,811 @@
1
+ import sqlite3
2
+ from datetime import datetime
3
+ import time
4
+ import sys
5
+ import threading
6
+ from pathlib import Path
7
+ from functools import wraps
8
+ import json
9
+ from datetime import datetime, timedelta
10
+ import re
11
+
12
+ from ..common.utils import *
13
+ from .device_manager import get_all_devices, set_device, get_all_devices_str, start_transmitter, terminate_transmitter, get_transmitter_state
14
+ from .acc_perms import perms, userperms
15
+ from .user_group_handler import user_group_handler
16
+
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ allow_connections = True
22
+
23
+ class ReservationHandler:
24
+ def __init__(self):
25
+ self.filepath = get_db_dir() / "reservations.db"
26
+ # self.filepath = Path(__file__).resolve().parent.parent/'db'/'reservations.db'
27
+
28
+ self.db = sqlite3.connect(self.filepath)
29
+ self.cursor = self.db.cursor()
30
+
31
+ self.cursor.execute('''
32
+ CREATE TABLE IF NOT EXISTS Accounts (
33
+ acc_id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ username TEXT NOT NULL UNIQUE,
35
+ email TEXT NOT NULL UNIQUE,
36
+ pass_salt TEXT NOT NULL,
37
+ pass_hash TEXT NOT NULL,
38
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
39
+ )
40
+ ''')
41
+
42
+ self.cursor.execute('''
43
+ CREATE TABLE IF NOT EXISTS Reservations (
44
+ res_id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ username TEXT NOT NULL,
46
+ device_id INTEGER NOT NULL,
47
+ start_time TIMESTAMP NOT NULL,
48
+ end_time TIMESTAMP NOT NULL,
49
+ salt TEXT NOT NULL,
50
+ hash TEXT NOT NULL
51
+ )
52
+ ''')
53
+
54
+ self.cursor.execute('''
55
+ CREATE INDEX IF NOT EXISTS idx_reservations_device_id ON Reservations(device_id);
56
+ ''')
57
+
58
+ self.cursor.close()
59
+ self.db.commit()
60
+ self.db.close()
61
+ self.lock = threading.RLock()
62
+
63
+ def handle_call(self, *, function_name, args):
64
+ if not allow_connections:
65
+ return {"ACC": map_arg("Server is not accepting connections at the moment. Please try later.")}
66
+ try:
67
+ if function_name == 'create_user':
68
+ return self.create_user(**args)
69
+ elif function_name == 'reserve_device':
70
+ username = unmap_arg(args['un'])
71
+ password = unmap_arg(args['pw'])
72
+ if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
73
+ starttime = datetime.fromtimestamp(unmap_arg(args['st']))
74
+ endtime = datetime.fromtimestamp(unmap_arg(args['et']))
75
+ device_id = unmap_arg(args['dd'])
76
+ response = self.reserve_device(username, device_id, starttime, endtime)
77
+ self.update_devices()
78
+ return response
79
+ else:
80
+ return {"ace": map_arg("Invalid login credentials.")}
81
+ elif function_name == 'get_res':
82
+ username = unmap_arg(args['un'])
83
+ password = unmap_arg(args['pw'])
84
+ if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
85
+ return self.grab_all_reservations(username)
86
+ else:
87
+ return {"ace": map_arg("Invalid login credentials.")}
88
+ elif function_name == 'get_dev':
89
+ username = unmap_arg(args['un'])
90
+ password = unmap_arg(args['pw'])
91
+ if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
92
+ return self.grab_all_devices(username)
93
+ else:
94
+ return {"ace": map_arg("Invalid login credentials.")}
95
+ elif function_name == 'login':
96
+ return self.login_user(unmap_arg(args['un']), unmap_arg(args['pw']))
97
+ elif function_name == 'cancel_res':
98
+ username = unmap_arg(args['un'])
99
+ password = unmap_arg(args['pw'])
100
+ if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
101
+ if self.validate_cancel_reservation(unmap_arg(args['res_id']), username):
102
+ return self.remove_reservation(res_id=unmap_arg(args['res_id']))
103
+ else:
104
+ return {"ace": map_arg("Invalid reservation ID or user does not own reservation.")}
105
+ else:
106
+ return {"ace": map_arg("Invalid login credentials.")}
107
+ # elif function_name == 'get_perms':
108
+ # username = unmap_arg(args['un'])
109
+ # password = unmap_arg(args['pw'])
110
+ # if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
111
+ # data = perms.get_perms(username)
112
+ # if data[0][0] == 'Normal User':
113
+ # return {'UC': map_arg(str(data)), 'details': map_arg(str(perms.userperms))}
114
+ # return {'UC': map_arg(str(data))}
115
+ # else:
116
+ # return {"ace": map_arg("Invalid login credentials.")}
117
+
118
+ elif function_name == "get_perms":
119
+ username = unmap_arg(args["un"])
120
+ password = unmap_arg(args["pw"])
121
+
122
+ if self.login_user(username, password) == {"UC": map_arg(f"{username}")}:
123
+ data = perms.get_perms(username)
124
+
125
+ if data and data[0] and data[0][0] == "Normal User":
126
+ uid = int(self.get_uid(username))
127
+
128
+ # Force pure ints (defensive)
129
+ devs_raw = user_group_handler.get_users_devices(uid=uid) or []
130
+ devs: list[int] = []
131
+ for d in devs_raw:
132
+ try:
133
+ devs.append(int(d))
134
+ except Exception:
135
+ continue
136
+ devs = sorted(set(devs))
137
+
138
+ # caps: force string keys for JSON + client lookups
139
+ caps: dict[str, dict] = {}
140
+ for d in devs:
141
+ _allowed_t, max_t = user_group_handler.get_users_max_reservation_time(uid=uid, device_id=d)
142
+ _allowed_r, max_r = user_group_handler.get_users_max_reservations(uid=uid, device_id=d)
143
+ caps[str(d)] = {
144
+ "max_reservation_time_sec": int(max_t),
145
+ "max_reservations": int(max_r),
146
+ }
147
+
148
+ groups_raw = user_group_handler.get_groups_user_in(uid=uid) or []
149
+ groups: list[str] = []
150
+ for g in groups_raw:
151
+ if g is None:
152
+ continue
153
+ s = str(g).strip()
154
+ if s:
155
+ groups.append(s)
156
+
157
+ return {
158
+ "UC": map_arg(str(data)),
159
+ "details": map_arg(json.dumps({
160
+ "devices": devs,
161
+ "caps": caps,
162
+ "groups": groups,
163
+ })),
164
+ }
165
+
166
+ return {"UC": map_arg(str(data))}
167
+
168
+ return {"ace": map_arg("Invalid login credentials.")}
169
+
170
+
171
+
172
+ elif function_name == 'set_enroll':
173
+ username = unmap_arg(args['un'])
174
+ password = unmap_arg(args['pw'])
175
+ if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
176
+ code_name = unmap_arg(args['ec'])
177
+ acc_id = self.get_uid(username)
178
+ if user_group_handler.enroll_user_with_code(uid=acc_id, code_name=code_name):
179
+ return {'UC': map_arg(f'Enrolled {username} with code {code_name}')}
180
+ else:
181
+ return {'UE': map_arg(f'Failed to enroll {username} with code {code_name}. Invalid or expired code.')}
182
+ else:
183
+ return {"ace": map_arg("Invalid login credentials.")}
184
+ return {"ace": map_arg(f"No matching server function to call: {function_name}")}
185
+ except Exception as e:
186
+ return {"RPC Acc Error", map_arg(f'{e}')}
187
+
188
+ @db_connection
189
+ def get_uid(self, username:str) -> int:
190
+ self.cursor.execute("SELECT acc_id FROM Accounts WHERE username = ? LIMIT 1", (username,))
191
+ row = self.cursor.fetchone()
192
+ uid = int(row[0]) if row else -1
193
+ return uid
194
+
195
+ @db_connection
196
+ def create_user(self,*, un, pw, em, ec, ad:bool=False):
197
+ un = unmap_arg(un)
198
+ pw = unmap_arg(pw)
199
+ em = unmap_arg(em)
200
+ ec = unmap_arg(ec)
201
+
202
+ # check if username and password fit the bill
203
+ if len(un) < 3:
204
+ return {"UE": map_arg("Username must be at least 3 characters long.")}
205
+ if len(pw) < 5:
206
+ return {"UE": map_arg("Password must be at least 5 characters long.")}
207
+ if '@' not in em:
208
+ return {"UE": map_arg("Invalid email address.")}
209
+
210
+ try:
211
+ if not user_group_handler.validate_enrollment_code(code_name=ec):
212
+ return {"UE": map_arg("Invalid or expired enrollment code.")}
213
+ except Exception:
214
+ return {"UE": map_arg("Invalid or expired enrollment code.")}
215
+
216
+ try:
217
+ passwords = hash_token(pw)
218
+ self.cursor.execute('''
219
+ INSERT INTO Accounts (username, email, pass_salt, pass_hash, created_at) VALUES (?, ?, ?, ?, ?)
220
+ ''', (un, em, passwords[0], passwords[1], datetime.now()))
221
+ uid = int(self.cursor.lastrowid)
222
+ user_group_handler.enroll_user_with_code(uid=uid, code_name=ec)
223
+ perms.set_user(un) # set perms
224
+ return {"UC": map_arg(f'{un}')}
225
+ except Exception as e:
226
+ return {"UE": map_arg(f'{e}')}
227
+
228
+ @db_connection
229
+ def remove_user(self, *, username):
230
+ self.cursor.execute('DELETE FROM Accounts WHERE username = ?', (username,))
231
+ perms.delete_user_from_reservation_handler(username)
232
+ return {"UC": map_arg(f'Removed {username}')}
233
+
234
+ @db_connection
235
+ def validate_cancel_reservation(self, res_id:int, username:str) -> bool:
236
+ self.cursor.execute('SELECT * FROM Reservations WHERE res_id = ?', (res_id,))
237
+ reservation = self.cursor.fetchone()
238
+ if reservation is not None and reservation[1] == username:
239
+ return True
240
+ return False
241
+
242
+ @db_connection
243
+ def remove_reservation(self, *, res_id):
244
+ self.cursor.execute('DELETE FROM Reservations WHERE res_id = ?', (res_id,))
245
+ return {"UC": map_arg(f'Removed reservation {res_id}')}
246
+
247
+ @db_connection
248
+ def rm_db(self):
249
+ if input("Drop Tables? (y/n): ") == 'y' and input("Are you sure? This deletes ALL data stored on the server. (y/n): ") == 'y':
250
+ time.sleep(1)
251
+ if input("Last chance. Are you sure? (y/n): ") == 'y':
252
+ self.cursor.execute('DROP TABLE IF EXISTS Accounts')
253
+ self.cursor.execute('DROP TABLE IF EXISTS Reservations')
254
+ perms.rm_db()
255
+ sys.exit()
256
+
257
+ @db_connection
258
+ def remove_all_users(self):
259
+ self.cursor.execute('DELETE FROM Accounts')
260
+ print("All accounts removed.")
261
+
262
+ @db_connection
263
+ def remove_all_reservations(self):
264
+ self.cursor.execute('DELETE FROM Reservations')
265
+ print("All reservations removed.")
266
+
267
+ @db_connection
268
+ def login_user(self, username, password):
269
+ self.cursor.execute('SELECT * FROM Accounts WHERE username = ?', (username,))
270
+ account = self.cursor.fetchone()
271
+
272
+ if account is not None and validate_token(account[3], account[4], password):
273
+ return {"UC": map_arg(f"{username}")}
274
+ else:
275
+ return {"UE": map_arg("Invalid login details.")}
276
+
277
+ def _clean_expired_reservations(self, device_id: int):
278
+ """Remove expired reservations from the database."""
279
+ current_time = datetime.now()
280
+ cur = self.db.cursor()
281
+ try:
282
+ cur.execute('DELETE FROM Reservations WHERE device_id = ? AND end_time < ?', (device_id, current_time))
283
+ finally:
284
+ cur.close()
285
+
286
+ @db_connection
287
+ def get_reserved_device_ids_for_user(self, username: str) -> list[int]:
288
+ """
289
+ Returns one device_id per reservation row that is still active/upcoming.
290
+ Duplicates are intentional (counts reservations, not unique devices).
291
+ """
292
+ try:
293
+ username = str(username).strip()
294
+ if not username:
295
+ return []
296
+
297
+ now = datetime.now()
298
+ self.cursor.execute(
299
+ """
300
+ SELECT device_id
301
+ FROM Reservations
302
+ WHERE username = ?
303
+ AND end_time > ?
304
+ """,
305
+ (username, now),
306
+ )
307
+ rows = self.cursor.fetchall() or []
308
+
309
+ out: list[int] = []
310
+ for (d,) in rows:
311
+ try:
312
+ out.append(int(d))
313
+ except Exception:
314
+ pass
315
+ return out
316
+ except Exception:
317
+ return []
318
+
319
+ @db_connection
320
+ def reserve_device(self, username, device_id: int, start_time, end_time, is_server=False):
321
+
322
+ self._clean_expired_reservations(device_id)
323
+
324
+ self.cursor.execute("SELECT acc_id FROM Accounts WHERE username = ? LIMIT 1", (username,))
325
+ row = self.cursor.fetchone()
326
+ accid = int(row[0]) if row else -1
327
+
328
+ # Check if reservation is valid with perms.
329
+ reserved_device_ids = self.get_reserved_device_ids_for_user(username)
330
+ reserved_device_ids = reserved_device_ids + [int(device_id)]
331
+
332
+ validation = perms.validate_reservation_request(
333
+ accid,
334
+ username,
335
+ device_id,
336
+ start_time,
337
+ end_time,
338
+ self.get_reservation_count(username),
339
+ is_server=is_server,
340
+ reserved_device_ids=reserved_device_ids,
341
+ )
342
+
343
+ if validation[0] == False:
344
+ return {"ace": map_arg(validation[1])}
345
+
346
+ # Check if the time slot is already reserved
347
+ self.cursor.execute('SELECT * FROM Reservations WHERE device_id = ? AND start_time < ? AND end_time > ?', (device_id, end_time, start_time))
348
+
349
+ if self.cursor.fetchone():
350
+ # Time slot is already reserved
351
+ return {"ace": map_arg("Device already reserved at this time. Please select another time or device.")}
352
+
353
+ token = generate_token()
354
+
355
+ # Store the reservation and hashed API token in the database
356
+ self.cursor.execute('INSERT INTO Reservations (username, device_id, start_time, end_time, salt, hash) VALUES (?, ?, ?, ?, ?, ?)',
357
+ (username, device_id, start_time, end_time, token[0], token[1]))
358
+ return {"Token": map_arg(token[2])}
359
+
360
+ @db_connection
361
+ def validate_token(self, *, token, device_id):
362
+
363
+ # check if the token is valid for the given device_id
364
+ # return 'Valid' if token is valid for the current time stamp (kick any users using atm)
365
+ # return 'Unocc' if no one is using the device at the current time stamp (past 5 minutes)
366
+ # return 'Invalid' o.w.
367
+
368
+ # TODO: Set for const lookup time for the api token? and perhaps check every minute to see if the token is still valid and then clean out of the list. HAVE the cleaning be on a separate thread.
369
+
370
+ # TODO: Store the token on the device itself in device manager, and then when the device is being used, it checks itself. Once the time is over, then tokens get replaced and cleaned out on the devices, and so subsquent calls cannot go through.
371
+
372
+ now = datetime.datetime.now()
373
+ cursor = self.db.cursor()
374
+
375
+ # Get the reservation details from the database
376
+ cursor.execute("""
377
+ SELECT * FROM Reservations WHERE start_time < ? AND end_time > ?
378
+ """, (device_id, now, now))
379
+
380
+ reservations = cursor.fetchone()
381
+ cursor.close()
382
+
383
+ if reservations is not None and validate_token(reservations[4], reservations[5], token):
384
+ return "Valid"
385
+ elif reservations is None:
386
+ return "Unocc"
387
+ else:
388
+ return "Invalid"
389
+
390
+ @db_connection
391
+ def grab_all_reservations_server(self):
392
+ self.cursor.execute('SELECT * FROM Reservations')
393
+ reservations = self.cursor.fetchall()
394
+ result = {}
395
+ for res in reservations:
396
+ result[int(res[0])] = res[1:-2]
397
+ return result
398
+
399
+ @db_connection
400
+ def grab_all_reservations(self, username:str, current_time=datetime.now()):
401
+ self.cursor.execute('SELECT * FROM Reservations')
402
+ reservations = self.cursor.fetchall()
403
+ perm = perms.get_perms(username)[0]
404
+ result = {}
405
+ if perm[0] == 'Admin':
406
+ for i, res in enumerate(reservations, start=1):
407
+ result[f'{res[0]}'] = map_arg(','.join(map(str, res[1:-2])))
408
+
409
+ # elif perm[0] == 'Normal User':
410
+ # normalperms = perms.userperms.devices_allowed
411
+ # for i, res in enumerate(reservations, start=1):
412
+ # if res[2] in normalperms:
413
+ # result[f'{res[0]}'] = map_arg(','.join(map(str, res[1:-2])))
414
+
415
+ elif perm[0] == 'Normal User':
416
+ self.cursor.execute("SELECT acc_id FROM Accounts WHERE username = ? LIMIT 1", (username,))
417
+ row = self.cursor.fetchone()
418
+ uid = int(row[0]) if row else -1
419
+ allowed = set(user_group_handler.get_users_devices(uid=uid))
420
+
421
+ for i, res in enumerate(reservations, start=1):
422
+ if int(res[2]) in allowed:
423
+ result[f'{res[0]}'] = map_arg(','.join(map(str, res[1:-2])))
424
+
425
+
426
+ elif perm[0] == 'Power User':
427
+ for i, res in enumerate(reservations, start=1):
428
+ if res[2] in str_to_list(perm[5]):
429
+ result[f'{res[0]}'] = map_arg(','.join(map(str, res[1:-2])))
430
+
431
+ return result
432
+
433
+ def grab_all_devices(self, username:str):
434
+ devices = get_all_devices_str()
435
+ perm = perms.get_perms(username)[0]
436
+ if perm[0] == 'Admin':
437
+ return {str(k): map_arg(str(v)) for k, v in devices.items()}
438
+
439
+ # if perm[0] == 'Normal User':
440
+ # normalperms = perms.userperms.devices_allowed
441
+ # return {str(k): map_arg(str(v)) for k, v in devices.items() if k in normalperms}
442
+
443
+ if perm[0] == 'Normal User':
444
+ uid = self.get_uid(username)
445
+ allowed = set(user_group_handler.get_users_devices(uid=uid))
446
+ return {str(k): map_arg(str(v)) for k, v in devices.items() if k in allowed}
447
+
448
+ if perm[0] == 'Power User':
449
+ return {str(k): map_arg(str(v)) for k, v in devices.items() if k in str_to_list(perm[5])}
450
+
451
+
452
+ @db_connection
453
+ def print_all_reservations(self):
454
+ print("Printing all reservations...")
455
+ cursor = self.db.cursor()
456
+ cursor.execute("""SELECT * FROM Reservations""")
457
+ reservations = cursor.fetchall()
458
+ cursor.close()
459
+ if reservations is not None:
460
+ for reservation in reservations:
461
+ print("Id:", reservation[0],"Username:", reservation[1], " Device ID:", reservation[2], " Start Time:", reservation[3], " End Time:", reservation[4])
462
+ else:
463
+ print("No reservations found.")
464
+
465
+ @db_connection
466
+ def get_all_accounts(self):
467
+ cursor = self.db.cursor()
468
+ cursor.execute("SELECT * FROM Accounts")
469
+ accounts = cursor.fetchall()
470
+ cursor.close()
471
+ return accounts
472
+
473
+ @db_connection
474
+ def print_all_accounts(self):
475
+ print("Printing all accounts...")
476
+ cursor = self.db.cursor()
477
+ cursor.execute("SELECT * FROM Accounts")
478
+ accounts = cursor.fetchall()
479
+ cursor.close()
480
+ if accounts is not None:
481
+ for account in accounts:
482
+ print("Username:", account[1], f'<{perms.get_perms(account[1])[0][0]}>', " Email:", account[2], " Created At:", account[5])
483
+ else:
484
+ print("No accounts found.")
485
+
486
+ @db_connection
487
+ def get_obfuscated_device_reservations_grouped_past_days(self, days: int) -> dict[str, dict[str, int]]:
488
+ try:
489
+ days = int(days)
490
+ except Exception:
491
+ return {}
492
+
493
+ if days < 0:
494
+ return {}
495
+
496
+ now = datetime.now()
497
+ cutoff = now - timedelta(days=days)
498
+
499
+ def _parse_logged_int(v) -> int:
500
+ if isinstance(v, int):
501
+ return v
502
+ if isinstance(v, float):
503
+ return int(v)
504
+
505
+ s = str(v).strip()
506
+
507
+ # Handles strings like:
508
+ # "int64_value: 2"
509
+ # "int64_value: 1772587800"
510
+ if ":" in s:
511
+ s = s.split(":", 1)[1].strip()
512
+
513
+ # remove quotes/newlines if present
514
+ s = s.strip().strip('"').strip("'")
515
+ return int(float(s))
516
+
517
+ # Take a consistent snapshot of the log file under the same lock used by writers.
518
+ try:
519
+ with log_lock:
520
+ activity_log = _load_logs()
521
+ except Exception:
522
+ activity_log = []
523
+
524
+ out: dict[str, dict[str, int]] = {}
525
+
526
+ for entry in activity_log:
527
+ try:
528
+ if not isinstance(entry, dict):
529
+ continue
530
+
531
+ if str(entry.get("action", "")).strip() != "reserve_device":
532
+ continue
533
+
534
+ details = entry.get("details", {})
535
+ if not isinstance(details, dict):
536
+ continue
537
+
538
+ args = details.get("args", {})
539
+ result = details.get("result", {})
540
+
541
+ if not isinstance(args, dict) or not isinstance(result, dict):
542
+ continue
543
+
544
+ # Only count SUCCESSFUL reservations
545
+ if "Token" not in result:
546
+ continue
547
+
548
+ raw_device_id = args.get("dd")
549
+ raw_start = args.get("st")
550
+ raw_end = args.get("et")
551
+
552
+ if raw_device_id is None or raw_start is None or raw_end is None:
553
+ continue
554
+
555
+ device_id = _parse_logged_int(raw_device_id)
556
+ start_ts = _parse_logged_int(raw_start)
557
+ end_ts = _parse_logged_int(raw_end)
558
+
559
+ start_dt = datetime.fromtimestamp(start_ts)
560
+ end_dt = datetime.fromtimestamp(end_ts)
561
+
562
+ # Same overlap semantics as before
563
+ if end_dt < cutoff or start_dt > now:
564
+ continue
565
+
566
+ dev_key = str(device_id)
567
+ day_key = start_dt.date().isoformat()
568
+
569
+ if dev_key not in out:
570
+ out[dev_key] = {}
571
+
572
+ out[dev_key][day_key] = out[dev_key].get(day_key, 0) + 1
573
+
574
+ except Exception:
575
+ continue
576
+
577
+ return dict(sorted(out.items(), key=lambda kv: int(kv[0])))
578
+
579
+ @db_connection
580
+ def get_obfuscated_usage_summary(self) -> dict[str, int | float]:
581
+
582
+ int_re = re.compile(r'int64_value:\s*([0-9]+)')
583
+ str_re = re.compile(r'string_value:\s*"([^"]*)"')
584
+
585
+ def _parse_proto_value(v):
586
+ if isinstance(v, (int, float)):
587
+ return v
588
+ if not isinstance(v, str):
589
+ return v
590
+
591
+ m_int = int_re.search(v)
592
+ if m_int:
593
+ return int(m_int.group(1))
594
+
595
+ m_str = str_re.search(v)
596
+ if m_str:
597
+ return m_str.group(1)
598
+
599
+ return v
600
+
601
+ try:
602
+ with log_lock:
603
+ activity_log = _load_logs()
604
+ except Exception:
605
+ activity_log = []
606
+
607
+ accounts: set[str] = set()
608
+ total_reservations = 0
609
+ total_reserved_seconds = 0
610
+
611
+ for entry in activity_log:
612
+ try:
613
+ if not isinstance(entry, dict):
614
+ continue
615
+
616
+ action = entry.get("action")
617
+ username = entry.get("username")
618
+ details = entry.get("details", {}) or {}
619
+ args = details.get("args", {}) or {}
620
+ result = details.get("result", {}) or {}
621
+
622
+ if not isinstance(args, dict) or not isinstance(result, dict):
623
+ continue
624
+
625
+ # Count accounts from successful logins
626
+ if action == "login" and "UC" in result:
627
+ uc_name = _parse_proto_value(result.get("UC"))
628
+ if uc_name:
629
+ accounts.add(str(uc_name))
630
+ elif username:
631
+ accounts.add(str(username))
632
+
633
+ # Count reservations + reserved time
634
+ if action == "reserve_device":
635
+ total_reservations += 1
636
+
637
+ st = _parse_proto_value(args.get("st"))
638
+ et = _parse_proto_value(args.get("et"))
639
+
640
+ if isinstance(st, int) and isinstance(et, int) and et > st:
641
+ total_reserved_seconds += (et - st)
642
+
643
+ except Exception:
644
+ continue
645
+
646
+ return {
647
+ "account_count": len(accounts),
648
+ "total_reservations": int(total_reservations),
649
+ "total_reserved_hours": round(total_reserved_seconds / 3600.0, 1),
650
+ }
651
+
652
+ def get_reservation_count(self, username):
653
+ self.cursor.execute("SELECT COUNT(*) FROM Reservations WHERE username = ?", (username,))
654
+ count = self.cursor.fetchone()[0]
655
+ return count
656
+
657
+ # @db_connection
658
+ # def update_devices(self):
659
+ # devices = get_all_devices()
660
+ # for device_id in list(devices.keys()):
661
+ # self._clean_expired_reservations(device_id)
662
+
663
+ # self.cursor.execute('SELECT * FROM Reservations')
664
+ # reservations = self.cursor.fetchall()
665
+
666
+ # reservations_now = 0
667
+
668
+ # for reservation in reservations: # Update tokens for each device
669
+ # if datetime.strptime(reservation[3], '%Y-%m-%d %H:%M:%S') <= datetime.now() and datetime.strptime(reservation[4], '%Y-%m-%d %H:%M:%S') > datetime.now():
670
+ # del devices[reservation[2]]
671
+ # set_device(reservation[2], reservation[5], reservation[6])
672
+
673
+ # # if not get_transmitter_state(): # Turn transmitter on
674
+ # # start_transmitter()
675
+
676
+ # reservations_now += 1
677
+
678
+ # if reservations_now == 0: # Turn transmitter off if no resservations
679
+ # # terminate_transmitter()
680
+ # pass
681
+
682
+ # for device in devices:
683
+ # set_device(device, '', '')
684
+
685
+ from datetime import datetime
686
+
687
+ @db_connection
688
+ def update_devices(self):
689
+ devices = get_all_devices() # whatever your local inventory is
690
+
691
+ # Build a lookup: canonical string -> original key in `devices`
692
+ # (handles int keys, str keys, etc.)
693
+ key_by_str = {str(k).strip(): k for k in devices.keys()}
694
+
695
+ # Clean expired reservations for devices we actually know locally
696
+ for k in list(devices.keys()):
697
+ self._clean_expired_reservations(str(k).strip())
698
+
699
+ self.cursor.execute("SELECT * FROM Reservations")
700
+ reservations = self.cursor.fetchall()
701
+
702
+ now = datetime.now()
703
+ reservations_now = 0
704
+
705
+ for reservation in reservations:
706
+ start = datetime.strptime(reservation[3], "%Y-%m-%d %H:%M:%S")
707
+ end = datetime.strptime(reservation[4], "%Y-%m-%d %H:%M:%S")
708
+
709
+ if start <= now < end:
710
+ dev_id = str(reservation[2]).strip()
711
+
712
+ # If this reservation corresponds to a local device, remove it from the "clear later" list.
713
+ orig_key = key_by_str.pop(dev_id, None)
714
+ if orig_key is not None:
715
+ devices.pop(orig_key, None) # safe even if already removed
716
+
717
+ # Apply reservation token/state for this device_id (local or virtual)
718
+ set_device(dev_id, reservation[5] or "", reservation[6] or "")
719
+
720
+ reservations_now += 1
721
+
722
+ if reservations_now == 0:
723
+ # terminate_transmitter()
724
+ pass
725
+
726
+ # Clear tokens for remaining *local* devices that are not currently reserved
727
+ for k in devices.keys():
728
+ set_device(str(k).strip(), "", "")
729
+
730
+ start_transmitter()
731
+ reservation_handler = ReservationHandler()
732
+
733
+ # region Logging purposes
734
+
735
+ import numpy as np
736
+ import threading
737
+ from datetime import datetime
738
+ from pathlib import Path
739
+
740
+ # Path to your activity logs file
741
+ LOG_PATH = get_db_dir() / "activity_logs.npy"
742
+ log_lock = threading.Lock()
743
+
744
+ def _load_logs():
745
+ if LOG_PATH.exists():
746
+ return np.load(LOG_PATH, allow_pickle=True).tolist()
747
+ return []
748
+
749
+ def _save_logs(logs):
750
+ np.save(LOG_PATH, logs, allow_pickle=True)
751
+
752
+ def _append_log(entry: dict):
753
+ """Append a single log entry. Holds log_lock only during file I/O, not during the RPC call."""
754
+ with log_lock:
755
+ logs = _load_logs()
756
+ logs.append(entry)
757
+ _save_logs(logs)
758
+
759
+ # Store a reference to the original handle_call so we can wrap it
760
+ ReservationHandler._original_handle_call = ReservationHandler.handle_call
761
+
762
+ def sanitize_for_logging(obj):
763
+ """
764
+ Recursively converts non-primitive objects into a string
765
+ representation, so that the object becomes pickleable.
766
+ """
767
+ if isinstance(obj, (str, int, float, bool, type(None))):
768
+ return obj
769
+ elif isinstance(obj, dict):
770
+ return {sanitize_for_logging(key): sanitize_for_logging(value) for key, value in obj.items()}
771
+ elif isinstance(obj, list):
772
+ return [sanitize_for_logging(item) for item in obj]
773
+ elif isinstance(obj, tuple):
774
+ return tuple(sanitize_for_logging(item) for item in obj)
775
+ else:
776
+ return repr(obj)
777
+
778
+ def handle_call_wrapper(self, *, function_name, args):
779
+ """
780
+ Wrapper around the original handle_call.
781
+ The RPC call runs WITHOUT holding log_lock so a slow/stalled disk write
782
+ can never block the server. Logging happens after the call returns.
783
+ """
784
+ # Make a sanitized copy of args before the call (no lock needed here)
785
+ sanitized_args = sanitize_for_logging(dict(args))
786
+ if 'pw' in sanitized_args:
787
+ sanitized_args['pw'] = '*'
788
+
789
+ # Run the actual RPC — NOT inside log_lock
790
+ result = self._original_handle_call(function_name=function_name, args=args)
791
+
792
+ # Log asynchronously after the call completes
793
+ sanitized_result = sanitize_for_logging(result)
794
+ username = sanitize_for_logging(unmap_arg(args['un'])) if 'un' in args else 'N/A'
795
+ entry = {
796
+ "timestamp": datetime.now().isoformat(),
797
+ "action": function_name,
798
+ "username": username,
799
+ "details": {
800
+ "args": sanitized_args,
801
+ "result": sanitized_result
802
+ }
803
+ }
804
+ threading.Thread(target=_append_log, args=(entry,), daemon=True).start()
805
+
806
+ return result
807
+
808
+ # Replace the original handle_call with our wrapper
809
+ ReservationHandler.handle_call = handle_call_wrapper
810
+
811
+ # endregion