hwgdreqs 0.8.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.
- hwgdreqs/__init__.py +1 -0
- hwgdreqs/api_server.py +347 -0
- hwgdreqs/config.py +123 -0
- hwgdreqs/gdbrowser.py +17 -0
- hwgdreqs/logging_service.py +82 -0
- hwgdreqs/login_dialog.py +343 -0
- hwgdreqs/login_worker.py +43 -0
- hwgdreqs/main_window.py +966 -0
- hwgdreqs/queue_manager.py +540 -0
- hwgdreqs/session_worker.py +24 -0
- hwgdreqs/settings_dialog.py +995 -0
- hwgdreqs/twitch_auth.py +400 -0
- hwgdreqs/twitch_chat.py +327 -0
- hwgdreqs/youtube_auth.py +36 -0
- hwgdreqs/youtube_chat.py +276 -0
- hwgdreqs-0.8.0.dist-info/METADATA +55 -0
- hwgdreqs-0.8.0.dist-info/RECORD +20 -0
- hwgdreqs-0.8.0.dist-info/WHEEL +4 -0
- hwgdreqs-0.8.0.dist-info/entry_points.txt +2 -0
- hwgdreqs-0.8.0.dist-info/licenses/LICENSE +674 -0
hwgdreqs/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
hwgdreqs/api_server.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
7
|
+
from urllib.parse import parse_qs, urlparse
|
|
8
|
+
|
|
9
|
+
from hwgdreqs.queue_manager import QueueManager
|
|
10
|
+
from hwgdreqs.twitch_auth import TwitchSession, get_channel_moderate_enabled, ban_twitch_user
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _make_handler(queue: QueueManager, session: TwitchSession | None = None):
|
|
14
|
+
class Handler(BaseHTTPRequestHandler):
|
|
15
|
+
def log_message(self, format: str, *args) -> None:
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
def _send_json(self, payload: object, status: int = 200) -> None:
|
|
19
|
+
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
20
|
+
self.send_response(status)
|
|
21
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
22
|
+
self.send_header("Content-Length", str(len(data)))
|
|
23
|
+
self.end_headers()
|
|
24
|
+
self.wfile.write(data)
|
|
25
|
+
|
|
26
|
+
def _read_json(self) -> dict:
|
|
27
|
+
length_raw = self.headers.get("Content-Length")
|
|
28
|
+
if not length_raw:
|
|
29
|
+
return {}
|
|
30
|
+
try:
|
|
31
|
+
length = int(length_raw)
|
|
32
|
+
except ValueError:
|
|
33
|
+
return {}
|
|
34
|
+
if length <= 0:
|
|
35
|
+
return {}
|
|
36
|
+
raw = self.rfile.read(length)
|
|
37
|
+
try:
|
|
38
|
+
return json.loads(raw.decode("utf-8"))
|
|
39
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
def _params(self) -> dict[str, str]:
|
|
43
|
+
parsed = urlparse(self.path)
|
|
44
|
+
qs = parse_qs(parsed.query)
|
|
45
|
+
out: dict[str, str] = {}
|
|
46
|
+
for k, v in qs.items():
|
|
47
|
+
if v:
|
|
48
|
+
out[k] = v[0]
|
|
49
|
+
if self.command == "POST":
|
|
50
|
+
body = self._read_json()
|
|
51
|
+
for k, v in body.items():
|
|
52
|
+
if isinstance(v, str):
|
|
53
|
+
out[k] = v
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
def _find_entry(self, level_id: str):
|
|
57
|
+
for entry in queue.levels:
|
|
58
|
+
if str(entry.id) == level_id:
|
|
59
|
+
return entry
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def do_GET(self) -> None:
|
|
63
|
+
path = urlparse(self.path).path
|
|
64
|
+
if path == "/queue":
|
|
65
|
+
params = self._params()
|
|
66
|
+
if "row" in params:
|
|
67
|
+
try:
|
|
68
|
+
row = int(params["row"])
|
|
69
|
+
levels = queue.levels
|
|
70
|
+
if row >= 0 and row < len(levels):
|
|
71
|
+
self._send_json({"level": asdict(levels[row])})
|
|
72
|
+
else:
|
|
73
|
+
self._send_json({"ok": False, "error": "invalid_row"}, status=400)
|
|
74
|
+
except ValueError:
|
|
75
|
+
self._send_json({"ok": False, "error": "invalid_row"}, status=400)
|
|
76
|
+
else:
|
|
77
|
+
self._send_json({"levels": [asdict(e) for e in queue.levels]})
|
|
78
|
+
return
|
|
79
|
+
if path == "/current":
|
|
80
|
+
levels = queue.levels
|
|
81
|
+
self._send_json({"level": asdict(levels[0]) if levels else None})
|
|
82
|
+
return
|
|
83
|
+
if path == "/add":
|
|
84
|
+
from hwgdreqs.gdbrowser import fetch_level
|
|
85
|
+
params = self._params()
|
|
86
|
+
level_id = params.get("id") or params.get("level_id") or ""
|
|
87
|
+
if not level_id:
|
|
88
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
level_data = fetch_level(level_id)
|
|
92
|
+
if not level_data:
|
|
93
|
+
self._send_json({"ok": False, "error": "level_not_found"}, status=404)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
success = queue.add_level(
|
|
97
|
+
level_id=level_id,
|
|
98
|
+
name=level_data.get("name", ""),
|
|
99
|
+
author=level_data.get("author", ""),
|
|
100
|
+
difficulty=level_data.get("difficulty", ""),
|
|
101
|
+
requester="API",
|
|
102
|
+
platform="custom",
|
|
103
|
+
description=level_data.get("description", ""),
|
|
104
|
+
length=level_data.get("length", ""),
|
|
105
|
+
large=bool(level_data.get("large", False)),
|
|
106
|
+
two_player=bool(level_data.get("twoPlayer", False)),
|
|
107
|
+
disliked=bool(level_data.get("disliked", False)),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if success:
|
|
111
|
+
self._send_json({"ok": True})
|
|
112
|
+
else:
|
|
113
|
+
self._send_json({"ok": False, "error": "add_failed"}, status=400)
|
|
114
|
+
return
|
|
115
|
+
if path in ("/delete", "/banauthor", "/banrequester", "/blacklistlevel", "/clear", "/bantwitch"):
|
|
116
|
+
params = self._params()
|
|
117
|
+
level_id = params.get("id") or params.get("level_id") or ""
|
|
118
|
+
|
|
119
|
+
if path == "/delete":
|
|
120
|
+
if not level_id:
|
|
121
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
122
|
+
return
|
|
123
|
+
queue.remove_level(level_id)
|
|
124
|
+
self._send_json({"ok": True})
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if path == "/banrequester":
|
|
128
|
+
if not level_id:
|
|
129
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
130
|
+
return
|
|
131
|
+
entry = self._find_entry(level_id)
|
|
132
|
+
if not entry:
|
|
133
|
+
self._send_json({"ok": False, "error": "not_found"}, status=404)
|
|
134
|
+
return
|
|
135
|
+
queue.blacklist_requester(entry.requester)
|
|
136
|
+
self._send_json({"ok": True})
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
if path == "/banauthor":
|
|
140
|
+
if not level_id:
|
|
141
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
142
|
+
return
|
|
143
|
+
entry = self._find_entry(level_id)
|
|
144
|
+
if not entry:
|
|
145
|
+
self._send_json({"ok": False, "error": "not_found"}, status=404)
|
|
146
|
+
return
|
|
147
|
+
queue.blacklist_author(entry.author)
|
|
148
|
+
self._send_json({"ok": True})
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if path == "/blacklistlevel":
|
|
152
|
+
if not level_id:
|
|
153
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
154
|
+
return
|
|
155
|
+
queue.blacklist_level(level_id)
|
|
156
|
+
self._send_json({"ok": True})
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
if path == "/bantwitch":
|
|
160
|
+
if not level_id:
|
|
161
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
162
|
+
return
|
|
163
|
+
entry = self._find_entry(level_id)
|
|
164
|
+
if not entry:
|
|
165
|
+
self._send_json({"ok": False, "error": "not_found"}, status=404)
|
|
166
|
+
return
|
|
167
|
+
if not session:
|
|
168
|
+
self._send_json({"ok": False, "error": "no_twitch_session"}, status=400)
|
|
169
|
+
return
|
|
170
|
+
if not get_channel_moderate_enabled():
|
|
171
|
+
self._send_json({"ok": False, "error": "moderation_not_enabled"}, status=400)
|
|
172
|
+
return
|
|
173
|
+
if entry.requester.lower() == session.login.lower():
|
|
174
|
+
self._send_json({"ok": False, "error": "cannot_ban_self"}, status=400)
|
|
175
|
+
return
|
|
176
|
+
error = ban_twitch_user(session, entry.requester)
|
|
177
|
+
if error:
|
|
178
|
+
self._send_json({"ok": False, "error": error}, status=400)
|
|
179
|
+
else:
|
|
180
|
+
self._send_json({"ok": True})
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
queue.clear_queue()
|
|
184
|
+
self._send_json({"ok": True})
|
|
185
|
+
return
|
|
186
|
+
self._send_json({"ok": False, "error": "not_found"}, status=404)
|
|
187
|
+
|
|
188
|
+
def do_POST(self) -> None:
|
|
189
|
+
path = urlparse(self.path).path
|
|
190
|
+
params = self._params()
|
|
191
|
+
level_id = params.get("id") or params.get("level_id") or ""
|
|
192
|
+
|
|
193
|
+
if path == "/delete":
|
|
194
|
+
if not level_id:
|
|
195
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
196
|
+
return
|
|
197
|
+
queue.remove_level(level_id)
|
|
198
|
+
self._send_json({"ok": True})
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
if path == "/banrequester":
|
|
202
|
+
if not level_id:
|
|
203
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
204
|
+
return
|
|
205
|
+
entry = self._find_entry(level_id)
|
|
206
|
+
if not entry:
|
|
207
|
+
self._send_json({"ok": False, "error": "not_found"}, status=404)
|
|
208
|
+
return
|
|
209
|
+
queue.blacklist_requester(entry.requester)
|
|
210
|
+
self._send_json({"ok": True})
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
if path == "/banauthor":
|
|
214
|
+
if not level_id:
|
|
215
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
216
|
+
return
|
|
217
|
+
entry = self._find_entry(level_id)
|
|
218
|
+
if not entry:
|
|
219
|
+
self._send_json({"ok": False, "error": "not_found"}, status=404)
|
|
220
|
+
return
|
|
221
|
+
queue.blacklist_author(entry.author)
|
|
222
|
+
self._send_json({"ok": True})
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if path == "/blacklistlevel":
|
|
226
|
+
if not level_id:
|
|
227
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
228
|
+
return
|
|
229
|
+
queue.blacklist_level(level_id)
|
|
230
|
+
self._send_json({"ok": True})
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
if path == "/bantwitch":
|
|
234
|
+
if not level_id:
|
|
235
|
+
self._send_json({"ok": False, "error": "missing_id"}, status=400)
|
|
236
|
+
return
|
|
237
|
+
entry = self._find_entry(level_id)
|
|
238
|
+
if not entry:
|
|
239
|
+
self._send_json({"ok": False, "error": "not_found"}, status=404)
|
|
240
|
+
return
|
|
241
|
+
if not session:
|
|
242
|
+
self._send_json({"ok": False, "error": "no_twitch_session"}, status=400)
|
|
243
|
+
return
|
|
244
|
+
if not get_channel_moderate_enabled():
|
|
245
|
+
self._send_json({"ok": False, "error": "moderation_not_enabled"}, status=400)
|
|
246
|
+
return
|
|
247
|
+
if entry.requester.lower() == session.login.lower():
|
|
248
|
+
self._send_json({"ok": False, "error": "cannot_ban_self"}, status=400)
|
|
249
|
+
return
|
|
250
|
+
error = ban_twitch_user(session, entry.requester)
|
|
251
|
+
if error:
|
|
252
|
+
self._send_json({"ok": False, "error": error}, status=400)
|
|
253
|
+
else:
|
|
254
|
+
self._send_json({"ok": True})
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
if path == "/clear":
|
|
258
|
+
queue.clear_queue()
|
|
259
|
+
self._send_json({"ok": True})
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
self._send_json({"ok": False, "error": "not_found"}, status=404)
|
|
263
|
+
|
|
264
|
+
return Handler
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class ApiServer:
|
|
268
|
+
def __init__(self, queue: QueueManager) -> None:
|
|
269
|
+
self._queue = queue
|
|
270
|
+
self._local_httpd: ThreadingHTTPServer | None = None
|
|
271
|
+
self._local_thread: threading.Thread | None = None
|
|
272
|
+
self._network_httpd: ThreadingHTTPServer | None = None
|
|
273
|
+
self._network_thread: threading.Thread | None = None
|
|
274
|
+
self._session: TwitchSession | None = None
|
|
275
|
+
self._local_port: int = 6767
|
|
276
|
+
self._host_to_network: bool = False
|
|
277
|
+
self._network_port: int = 0
|
|
278
|
+
|
|
279
|
+
def set_config(self, local_port: int, host_to_network: bool, network_port: int) -> None:
|
|
280
|
+
restart = (self._local_port != local_port or \
|
|
281
|
+
self._host_to_network != host_to_network or \
|
|
282
|
+
self._network_port != network_port)
|
|
283
|
+
self._local_port = local_port
|
|
284
|
+
self._host_to_network = host_to_network
|
|
285
|
+
self._network_port = network_port
|
|
286
|
+
if restart:
|
|
287
|
+
self.stop()
|
|
288
|
+
self.start()
|
|
289
|
+
|
|
290
|
+
def set_session(self, session: TwitchSession | None) -> None:
|
|
291
|
+
self._session = session
|
|
292
|
+
self.stop()
|
|
293
|
+
self.start()
|
|
294
|
+
|
|
295
|
+
def start(self) -> bool:
|
|
296
|
+
success = False
|
|
297
|
+
# local server
|
|
298
|
+
if self._local_thread and self._local_thread.is_alive():
|
|
299
|
+
success = True
|
|
300
|
+
else:
|
|
301
|
+
handler = _make_handler(self._queue, self._session)
|
|
302
|
+
try:
|
|
303
|
+
self._local_httpd = ThreadingHTTPServer(("127.0.0.1", self._local_port), handler)
|
|
304
|
+
self._local_thread = threading.Thread(target=self._local_httpd.serve_forever, daemon=True)
|
|
305
|
+
self._local_thread.start()
|
|
306
|
+
success = True
|
|
307
|
+
except OSError:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
# network server if on
|
|
311
|
+
if self._host_to_network:
|
|
312
|
+
if not (self._network_thread and self._network_thread.is_alive()):
|
|
313
|
+
handler = _make_handler(self._queue, self._session)
|
|
314
|
+
try:
|
|
315
|
+
self._network_httpd = ThreadingHTTPServer(("0.0.0.0", self._network_port), handler)
|
|
316
|
+
self._network_thread = threading.Thread(target=self._network_httpd.serve_forever, daemon=True)
|
|
317
|
+
self._network_thread.start()
|
|
318
|
+
success = True
|
|
319
|
+
except OSError:
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
return success
|
|
323
|
+
|
|
324
|
+
def stop(self) -> None:
|
|
325
|
+
# stop local server
|
|
326
|
+
if self._local_httpd:
|
|
327
|
+
try:
|
|
328
|
+
self._local_httpd.shutdown()
|
|
329
|
+
except OSError:
|
|
330
|
+
pass
|
|
331
|
+
try:
|
|
332
|
+
self._local_httpd.server_close()
|
|
333
|
+
except OSError:
|
|
334
|
+
pass
|
|
335
|
+
self._local_httpd = None
|
|
336
|
+
|
|
337
|
+
# stop network server
|
|
338
|
+
if self._network_httpd:
|
|
339
|
+
try:
|
|
340
|
+
self._network_httpd.shutdown()
|
|
341
|
+
except OSError:
|
|
342
|
+
pass
|
|
343
|
+
try:
|
|
344
|
+
self._network_httpd.server_close()
|
|
345
|
+
except OSError:
|
|
346
|
+
pass
|
|
347
|
+
self._network_httpd = None
|
hwgdreqs/config.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
APP_NAME = "HwGDReqs"
|
|
11
|
+
APP_VERSION = "0.8.0"
|
|
12
|
+
|
|
13
|
+
TWITCH_CLIENT_ID = "hq65d75rdxry2cfjgemvydqp2vfr84"
|
|
14
|
+
TWITCH_SCOPES = ["chat:read", "user:read:email"]
|
|
15
|
+
TWITCH_CHAT_EDIT_SCOPE = "chat:edit"
|
|
16
|
+
TWITCH_CHANNEL_MODERATE_SCOPE = ["channel:moderate", "moderator:manage:banned_users"]
|
|
17
|
+
TWITCH_DEVICE_URL = "https://id.twitch.tv/oauth2/device"
|
|
18
|
+
TWITCH_TOKEN_URL = "https://id.twitch.tv/oauth2/token"
|
|
19
|
+
TWITCH_USERS_URL = "https://api.twitch.tv/helix/users"
|
|
20
|
+
TWITCH_IRC_HOST = "irc.chat.twitch.tv"
|
|
21
|
+
TWITCH_IRC_PORT = 6667
|
|
22
|
+
|
|
23
|
+
LEVEL_ID_PATTERN = r"\b(\d{7,9})\b"
|
|
24
|
+
COMMA_LEVEL_ID_PATTERN = r"\b(\d{1,3}(?:,\d{3})+)\b"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def app_root() -> Path:
|
|
28
|
+
if getattr(sys, "frozen", False):
|
|
29
|
+
# use _MEIPASS
|
|
30
|
+
return Path(sys._MEIPASS)
|
|
31
|
+
return Path(__file__).resolve().parent.parent
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def exec_dir() -> Path:
|
|
35
|
+
if getattr(sys, "frozen", False):
|
|
36
|
+
return Path(sys.executable).parent
|
|
37
|
+
return Path(__file__).resolve().parent.parent
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
import sys
|
|
41
|
+
import shutil
|
|
42
|
+
|
|
43
|
+
def data_dir() -> Path:
|
|
44
|
+
if sys.platform == "win32":
|
|
45
|
+
# Windows
|
|
46
|
+
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
47
|
+
new_path = base / APP_NAME
|
|
48
|
+
|
|
49
|
+
# old data chek
|
|
50
|
+
old_base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
|
|
51
|
+
old_path = old_base / APP_NAME
|
|
52
|
+
|
|
53
|
+
if old_path.exists() and not new_path.exists():
|
|
54
|
+
shutil.copytree(old_path, new_path)
|
|
55
|
+
# bye old data :3
|
|
56
|
+
shutil.rmtree(old_path)
|
|
57
|
+
|
|
58
|
+
new_path.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
return new_path
|
|
60
|
+
elif sys.platform == "darwin":
|
|
61
|
+
# macOS
|
|
62
|
+
base = Path.home() / "Library" / "Application Support" / APP_NAME
|
|
63
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
return base
|
|
65
|
+
else:
|
|
66
|
+
# Linux
|
|
67
|
+
base = Path.home() / ".config" / APP_NAME
|
|
68
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
return base
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def asset_path(name: str) -> Path:
|
|
73
|
+
return app_root() / "assets" / name
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def queue_file() -> Path:
|
|
77
|
+
return data_dir() / "data.json"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def token_file() -> Path:
|
|
81
|
+
return data_dir() / "auth.dat"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def encode_token(data: dict) -> str:
|
|
85
|
+
raw = json.dumps(data).encode("utf-8")
|
|
86
|
+
return base64.b64encode(raw).decode("ascii")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def decode_token(encoded: str) -> dict | None:
|
|
90
|
+
try:
|
|
91
|
+
raw = base64.b64decode(encoded.encode("ascii"))
|
|
92
|
+
return json.loads(raw.decode("utf-8"))
|
|
93
|
+
except (ValueError, json.JSONDecodeError):
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def save_auth(data: dict) -> None:
|
|
98
|
+
token_file().write_text(encode_token(data), encoding="utf-8")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_auth() -> dict | None:
|
|
102
|
+
path = token_file()
|
|
103
|
+
if not path.exists():
|
|
104
|
+
return None
|
|
105
|
+
return decode_token(path.read_text(encoding="utf-8"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def clear_auth() -> None:
|
|
109
|
+
path = token_file()
|
|
110
|
+
if path.exists():
|
|
111
|
+
path.unlink()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_local_ip() -> str:
|
|
115
|
+
"""Get the local IPv4 address of the machine."""
|
|
116
|
+
import socket
|
|
117
|
+
try:
|
|
118
|
+
# dummy socket to a public server to find the local IP
|
|
119
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
120
|
+
s.connect(("8.8.8.8", 80)) # Doesn't need to actually connect
|
|
121
|
+
return s.getsockname()[0]
|
|
122
|
+
except Exception:
|
|
123
|
+
return "127.0.0.1" # Fallback to localhost
|
hwgdreqs/gdbrowser.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
GDBROWSER_LEVEL_URL = "https://gdbrowser.com/api/level/{level_id}"
|
|
4
|
+
|
|
5
|
+
def fetch_level(level_id: str) -> dict | None:
|
|
6
|
+
try:
|
|
7
|
+
response = requests.get(
|
|
8
|
+
GDBROWSER_LEVEL_URL.format(level_id=level_id),
|
|
9
|
+
timeout=10,
|
|
10
|
+
)
|
|
11
|
+
response.raise_for_status()
|
|
12
|
+
data = response.json()
|
|
13
|
+
if not data.get("name"):
|
|
14
|
+
return None
|
|
15
|
+
return data
|
|
16
|
+
except (requests.RequestException, ValueError):
|
|
17
|
+
return None
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from hwgdreqs.config import data_dir
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_logger() -> logging.Logger:
|
|
10
|
+
log_dir = data_dir() / "logs"
|
|
11
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
|
|
13
|
+
log_file = log_dir / "hwgdreqs.log"
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("hwgdreqs")
|
|
16
|
+
if logger.handlers:
|
|
17
|
+
return logger
|
|
18
|
+
|
|
19
|
+
logger.setLevel(logging.INFO)
|
|
20
|
+
|
|
21
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
22
|
+
file_handler.setLevel(logging.INFO)
|
|
23
|
+
|
|
24
|
+
formatter = logging.Formatter(
|
|
25
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
26
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
27
|
+
)
|
|
28
|
+
file_handler.setFormatter(formatter)
|
|
29
|
+
|
|
30
|
+
logger.addHandler(file_handler)
|
|
31
|
+
|
|
32
|
+
return logger
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def log_level_added(level_id: str, level_name: str, requester: str, platform: str) -> None:
|
|
36
|
+
logger = get_logger()
|
|
37
|
+
logger.info(f"Level added | ID: {level_id} | Name: {level_name} | Requester: {requester} | Platform: {platform}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def log_level_deleted(level_id: str, level_name: str, requester: str) -> None:
|
|
41
|
+
logger = get_logger()
|
|
42
|
+
logger.info(f"Level deleted | ID: {level_id} | Name: {level_name} | Requested by: {requester}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def log_level_swapped(old_level_id: str, old_level_name: str, new_level_id: str, new_level_name: str) -> None:
|
|
46
|
+
logger = get_logger()
|
|
47
|
+
logger.info(f"Level swapped | Old ID: {old_level_id} ({old_level_name}) | New ID: {new_level_id} ({new_level_name})")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def log_requester_blacklisted(requester: str) -> None:
|
|
51
|
+
logger = get_logger()
|
|
52
|
+
logger.info(f"Requester blacklisted | {requester}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def log_level_blacklisted(level_id: str, level_name: str) -> None:
|
|
56
|
+
logger = get_logger()
|
|
57
|
+
logger.info(f"Level blacklisted | ID: {level_id} | Name: {level_name}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def log_author_blacklisted(author: str) -> None:
|
|
61
|
+
logger = get_logger()
|
|
62
|
+
logger.info(f"Author blacklisted | {author}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def log_requester_unblacklisted(requester: str) -> None:
|
|
66
|
+
logger = get_logger()
|
|
67
|
+
logger.info(f"Requester unblacklisted | {requester}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def log_level_unblacklisted(level_id: str) -> None:
|
|
71
|
+
logger = get_logger()
|
|
72
|
+
logger.info(f"Level unblacklisted | ID: {level_id}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def log_author_unblacklisted(author: str) -> None:
|
|
76
|
+
logger = get_logger()
|
|
77
|
+
logger.info(f"Author unblacklisted | {author}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def log_queue_cleared() -> None:
|
|
81
|
+
logger = get_logger()
|
|
82
|
+
logger.info("Queue cleared")
|