Files
linux-user-timer-native-app/desktop/client.py
T
2026-09-17 20:41:35 +05:00

934 lines
17 KiB
Python

import json
import socket
SOCKET_PATH = "/run/parental-control.sock"
WEEKDAY_NAMES = (
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
)
class IPCError(Exception):
pass
class AuthorizationError(IPCError):
pass
class DatabaseError(IPCError):
pass
def _gui_to_named_weekdays(
data,
):
"""
Convert the original GUI's numeric weekday
keys into the named weekday format used by
the daemon/configuration.
GUI:
0 = sunday
1 = monday
2 = tuesday
3 = wednesday
4 = thursday
5 = friday
6 = saturday
Config:
sunday
monday
...
saturday
"""
if not isinstance(
data,
dict,
):
return {}
converted = {}
for key, value in data.items():
try:
numeric_key = int(
key
)
except (
TypeError,
ValueError,
):
numeric_key = None
if (
numeric_key is not None
and 0 <= numeric_key < len(
WEEKDAY_NAMES
)
):
weekday = WEEKDAY_NAMES[
numeric_key
]
else:
weekday = str(
key
).lower().strip()
converted[
weekday
] = value
return converted
def _time_to_minutes(
value,
):
"""
Convert a time value into minutes since
midnight.
Accepted formats include:
"16:00" -> 960
"21:00" -> 1260
Integer values are treated as minutes.
"""
if isinstance(
value,
str,
):
value = value.strip()
parts = value.split(
":"
)
if len(parts) == 2:
try:
hours = int(
parts[0]
)
minutes = int(
parts[1]
)
if (
0 <= hours <= 23
and 0 <= minutes <= 59
):
return (
hours * 60
+ minutes
)
except (
TypeError,
ValueError,
):
pass
try:
return int(
value
)
except (
TypeError,
ValueError,
):
return None
try:
return int(
value
)
except (
TypeError,
ValueError,
):
return None
def _minutes_to_time(
value,
):
"""
Convert minutes since midnight into the
HH:MM string format used by the backend.
"""
try:
total_minutes = int(
value
)
except (
TypeError,
ValueError,
):
return None
if not (
0 <= total_minutes < 24 * 60
):
return None
hours = (
total_minutes // 60
)
minutes = (
total_minutes % 60
)
return (
f"{hours:02d}:"
f"{minutes:02d}"
)
def _gui_to_named_access_windows(
data,
):
"""
Convert access windows from the format
used by the original GUI into the format
expected by the daemon.
GUI format:
{
4: [
[960, 1260]
]
}
Backend format:
{
"thursday": [
{
"start": "16:00",
"end": "21:00"
}
]
}
"""
if not isinstance(
data,
dict,
):
return {}
converted = {}
for key, windows in data.items():
try:
numeric_key = int(
key
)
except (
TypeError,
ValueError,
):
numeric_key = None
if (
numeric_key is not None
and 0 <= numeric_key < len(
WEEKDAY_NAMES
)
):
weekday = WEEKDAY_NAMES[
numeric_key
]
else:
weekday = str(
key
).lower().strip()
if not isinstance(
windows,
list,
):
continue
normalized_windows = []
for window in windows:
if isinstance(
window,
dict,
):
start = window.get(
"start"
)
end = window.get(
"end"
)
elif isinstance(
window,
(list, tuple),
) and len(window) >= 2:
start = window[0]
end = window[1]
else:
continue
start_time = (
_minutes_to_time(
start
)
)
end_time = (
_minutes_to_time(
end
)
)
if (
start_time is None
or end_time is None
):
continue
normalized_windows.append(
{
"start": start_time,
"end": end_time,
}
)
if normalized_windows:
converted[
weekday
] = normalized_windows
return converted
def _named_to_gui_allowances(
data,
):
"""
Convert named weekday allowance keys from
the daemon into numeric weekday keys expected
by the original GUI.
"""
if not isinstance(
data,
dict,
):
return {}
converted = {}
for index, weekday in enumerate(
WEEKDAY_NAMES
):
value = data.get(
weekday,
0,
)
try:
value = int(
value
)
except (
TypeError,
ValueError,
):
value = 0
converted[
index
] = max(
0,
value,
)
return converted
def _named_to_gui_access_windows(
data,
):
"""
Convert access windows returned by the
daemon into the exact representation expected
by the original GUI.
Backend:
{
"thursday": [
{
"start": "16:00",
"end": "21:00"
}
]
}
GUI:
{
4: [
[960, 1260]
]
}
"""
if not isinstance(
data,
dict,
):
return {}
converted = {}
for index, weekday in enumerate(
WEEKDAY_NAMES
):
if weekday not in data:
continue
windows = data[
weekday
]
if not isinstance(
windows,
list,
):
continue
gui_windows = []
for window in windows:
if not isinstance(
window,
dict,
):
continue
start = _time_to_minutes(
window.get(
"start"
)
)
end = _time_to_minutes(
window.get(
"end"
)
)
if (
start is None
or end is None
):
continue
gui_windows.append(
[
start,
end,
]
)
if gui_windows:
converted[
index
] = gui_windows
return converted
class IPCClient:
def __init__(
self,
socket_path=SOCKET_PATH,
):
self.socket_path = socket_path
def _request(
self,
method,
args=None,
):
request = {
"method": method,
"args": args or {},
}
try:
with socket.socket(
socket.AF_UNIX,
socket.SOCK_STREAM,
) as sock:
sock.connect(
self.socket_path
)
payload = json.dumps(
request
).encode(
"utf-8"
)
sock.sendall(
payload
)
sock.shutdown(
socket.SHUT_WR
)
chunks = []
while True:
chunk = sock.recv(
4096
)
if not chunk:
break
chunks.append(
chunk
)
except OSError as exc:
raise IPCError(
str(exc)
) from exc
try:
response = json.loads(
b"".join(
chunks
).decode(
"utf-8"
)
)
except (
json.JSONDecodeError,
UnicodeDecodeError,
) as exc:
raise IPCError(
"Invalid response from "
"parental-control daemon"
) from exc
if response.get(
"ok"
):
return response.get(
"result"
)
error = response.get(
"error",
"Unknown error",
)
if response.get(
"error_type"
) == "authorization":
raise AuthorizationError(
error
)
if response.get(
"error_type"
) == "database":
raise DatabaseError(
error
)
raise IPCError(
error
)
def get_users(
self,
):
return self._request(
"get_users"
)
def get_user(
self,
user_id,
):
return self._request(
"get_user",
{
"user_id": int(
user_id
),
},
)
def get_remaining_time(
self,
user_id,
):
return self._request(
"get_remaining_time",
{
"user_id": int(
user_id
),
},
)
def get_allowances(
self,
user_id,
):
result = self._request(
"get_allowances",
{
"user_id": int(
user_id
),
},
)
return _named_to_gui_allowances(
result
)
def get_access_windows(
self,
user_id,
):
result = self._request(
"get_access_windows",
{
"user_id": int(
user_id
),
},
)
return _named_to_gui_access_windows(
result
)
def create_user(
self,
username,
enabled,
allowances,
access_windows,
):
return self._request(
"create_user",
{
"username": username,
"enabled": bool(
enabled
),
"allowances": (
_gui_to_named_weekdays(
allowances
)
),
"access_windows": (
_gui_to_named_access_windows(
access_windows
)
),
},
)
def update_user(
self,
user_id,
enabled,
allowances,
access_windows,
):
return self._request(
"update_user",
{
"user_id": int(
user_id
),
"enabled": bool(
enabled
),
"allowances": (
_gui_to_named_weekdays(
allowances
)
),
"access_windows": (
_gui_to_named_access_windows(
access_windows
)
),
},
)
def delete_user(
self,
user_id,
):
return self._request(
"delete_user",
{
"user_id": int(
user_id
),
},
)
def set_enabled(
self,
user_id,
enabled,
):
return self._request(
"set_enabled",
{
"user_id": int(
user_id
),
"enabled": bool(
enabled
),
},
)
def set_allowance(
self,
user_id,
weekday,
seconds,
):
return self._request(
"set_allowance",
{
"user_id": int(
user_id
),
"weekday": str(
weekday
).lower(),
"seconds": int(
seconds
),
},
)
def set_access_windows(
self,
user_id,
weekday,
windows,
):
return self._request(
"set_access_windows",
{
"user_id": int(
user_id
),
"weekday": str(
weekday
).lower(),
"windows": windows,
},
)
def add_temporary_time(
self,
user_id,
seconds,
):
return self._request(
"add_temporary_time",
{
"user_id": int(
user_id
),
"seconds": int(
seconds
),
},
)
class Database:
"""
Compatibility interface for the original GUI.
The original PySide6 GUI calls Database methods
directly on the class. These methods route
operations through the parental-control daemon.
"""
_client = IPCClient()
@classmethod
def get_users(
cls,
):
return cls._client.get_users()
@classmethod
def get_user(
cls,
user_id,
):
return cls._client.get_user(
user_id
)
@classmethod
def get_remaining_time(
cls,
user_id,
):
return cls._client.get_remaining_time(
user_id
)
@classmethod
def get_allowances(
cls,
user_id,
):
return cls._client.get_allowances(
user_id
)
@classmethod
def get_access_windows(
cls,
user_id,
):
return cls._client.get_access_windows(
user_id
)
@classmethod
def create_user(
cls,
username,
enabled,
allowances,
access_windows,
):
return cls._client.create_user(
username,
enabled,
allowances,
access_windows,
)
@classmethod
def update_user(
cls,
user_id,
enabled,
allowances,
access_windows,
):
return cls._client.update_user(
user_id,
enabled,
allowances,
access_windows,
)
@classmethod
def delete_user(
cls,
user_id,
):
return cls._client.delete_user(
user_id
)
@classmethod
def set_enabled(
cls,
user_id,
enabled,
):
return cls._client.set_enabled(
user_id,
enabled,
)
@classmethod
def set_allowance(
cls,
user_id,
weekday,
seconds,
):
return cls._client.set_allowance(
user_id,
weekday,
seconds,
)
@classmethod
def set_access_windows(
cls,
user_id,
weekday,
windows,
):
return cls._client.set_access_windows(
user_id,
weekday,
windows,
)
@classmethod
def add_temporary_time(
cls,
user_id,
seconds,
):
return cls._client.add_temporary_time(
user_id,
seconds,
)