pygpt-net 2.6.1__py3-none-any.whl → 2.6.2__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 (61) hide show
  1. pygpt_net/CHANGELOG.txt +4 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +15 -1
  4. pygpt_net/controller/chat/response.py +5 -3
  5. pygpt_net/controller/chat/stream.py +40 -2
  6. pygpt_net/controller/plugins/plugins.py +25 -0
  7. pygpt_net/controller/presets/editor.py +33 -88
  8. pygpt_net/controller/presets/experts.py +20 -1
  9. pygpt_net/controller/presets/presets.py +2 -2
  10. pygpt_net/controller/ui/mode.py +17 -66
  11. pygpt_net/core/agents/runner.py +15 -7
  12. pygpt_net/core/experts/experts.py +3 -3
  13. pygpt_net/data/config/config.json +3 -3
  14. pygpt_net/data/config/models.json +3 -3
  15. pygpt_net/data/locale/locale.de.ini +2 -0
  16. pygpt_net/data/locale/locale.en.ini +2 -0
  17. pygpt_net/data/locale/locale.es.ini +2 -0
  18. pygpt_net/data/locale/locale.fr.ini +2 -0
  19. pygpt_net/data/locale/locale.it.ini +2 -0
  20. pygpt_net/data/locale/locale.pl.ini +3 -1
  21. pygpt_net/data/locale/locale.uk.ini +2 -0
  22. pygpt_net/data/locale/locale.zh.ini +2 -0
  23. pygpt_net/plugin/base/plugin.py +35 -3
  24. pygpt_net/plugin/bitbucket/__init__.py +12 -0
  25. pygpt_net/plugin/bitbucket/config.py +267 -0
  26. pygpt_net/plugin/bitbucket/plugin.py +125 -0
  27. pygpt_net/plugin/bitbucket/worker.py +569 -0
  28. pygpt_net/plugin/facebook/__init__.py +12 -0
  29. pygpt_net/plugin/facebook/config.py +359 -0
  30. pygpt_net/plugin/facebook/plugin.py +114 -0
  31. pygpt_net/plugin/facebook/worker.py +698 -0
  32. pygpt_net/plugin/github/__init__.py +12 -0
  33. pygpt_net/plugin/github/config.py +441 -0
  34. pygpt_net/plugin/github/plugin.py +124 -0
  35. pygpt_net/plugin/github/worker.py +674 -0
  36. pygpt_net/plugin/google/__init__.py +12 -0
  37. pygpt_net/plugin/google/config.py +367 -0
  38. pygpt_net/plugin/google/plugin.py +126 -0
  39. pygpt_net/plugin/google/worker.py +826 -0
  40. pygpt_net/plugin/slack/__init__.py +12 -0
  41. pygpt_net/plugin/slack/config.py +349 -0
  42. pygpt_net/plugin/slack/plugin.py +116 -0
  43. pygpt_net/plugin/slack/worker.py +639 -0
  44. pygpt_net/plugin/telegram/__init__.py +12 -0
  45. pygpt_net/plugin/telegram/config.py +308 -0
  46. pygpt_net/plugin/telegram/plugin.py +118 -0
  47. pygpt_net/plugin/telegram/worker.py +563 -0
  48. pygpt_net/plugin/twitter/__init__.py +12 -0
  49. pygpt_net/plugin/twitter/config.py +491 -0
  50. pygpt_net/plugin/twitter/plugin.py +126 -0
  51. pygpt_net/plugin/twitter/worker.py +837 -0
  52. pygpt_net/provider/agents/llama_index/legacy/openai_assistant.py +35 -3
  53. pygpt_net/ui/base/config_dialog.py +4 -0
  54. pygpt_net/ui/dialog/preset.py +34 -77
  55. pygpt_net/ui/layout/toolbox/presets.py +2 -2
  56. pygpt_net/ui/main.py +3 -1
  57. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/METADATA +145 -2
  58. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/RECORD +61 -33
  59. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/LICENSE +0 -0
  60. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/WHEEL +0 -0
  61. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,563 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.08.15 00:00:00 #
10
+ # ================================================== #
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import mimetypes
16
+ import os
17
+ import threading
18
+ import requests
19
+
20
+ from PySide6.QtCore import Slot
21
+
22
+ from pygpt_net.plugin.base.worker import BaseWorker, BaseSignals
23
+
24
+
25
+ class WorkerSignals(BaseSignals):
26
+ pass
27
+
28
+
29
+ class Worker(BaseWorker):
30
+ """
31
+ Telegram plugin worker: Bot API (simple HTTP) and User mode (Telethon).
32
+ Auto-login for user mode (sends code when possible), bot mode needs only bot_token.
33
+ """
34
+
35
+ def __init__(self, *args, **kwargs):
36
+ super(Worker, self).__init__()
37
+ self.signals = WorkerSignals()
38
+ self.args = args
39
+ self.kwargs = kwargs
40
+ self.plugin = None
41
+ self.cmds = None
42
+ self.ctx = None
43
+ self.msg = None
44
+ self._tl_client = None
45
+ self._tl_lock = threading.Lock()
46
+
47
+ # ---------------------- Core runner ----------------------
48
+
49
+ @Slot()
50
+ def run(self):
51
+ try:
52
+ responses = []
53
+ for item in self.cmds:
54
+ if self.is_stopped():
55
+ break
56
+ try:
57
+ response = None
58
+ if item["cmd"] in self.plugin.allowed_cmds and self.plugin.has_cmd(item["cmd"]):
59
+
60
+ # -------- Auth (user mode) --------
61
+ if item["cmd"] == "tg_login_begin":
62
+ response = self.cmd_tg_login_begin(item)
63
+ elif item["cmd"] == "tg_login_complete":
64
+ response = self.cmd_tg_login_complete(item)
65
+ elif item["cmd"] == "tg_logout":
66
+ response = self.cmd_tg_logout(item)
67
+
68
+ # -------- Info --------
69
+ elif item["cmd"] == "tg_mode":
70
+ response = self.cmd_tg_mode(item)
71
+ elif item["cmd"] == "tg_me":
72
+ response = self.cmd_tg_me(item)
73
+
74
+ # -------- Messaging --------
75
+ elif item["cmd"] == "tg_send_message":
76
+ response = self.cmd_tg_send_message(item)
77
+ elif item["cmd"] == "tg_send_photo":
78
+ response = self.cmd_tg_send_photo(item)
79
+ elif item["cmd"] == "tg_send_document":
80
+ response = self.cmd_tg_send_document(item)
81
+
82
+ # -------- Chats --------
83
+ elif item["cmd"] == "tg_get_chat":
84
+ response = self.cmd_tg_get_chat(item)
85
+
86
+ # -------- Updates / Files (bot) --------
87
+ elif item["cmd"] == "tg_get_updates":
88
+ response = self.cmd_tg_get_updates(item)
89
+ elif item["cmd"] == "tg_download_file":
90
+ response = self.cmd_tg_download_file(item)
91
+
92
+ # -------- Contacts / Dialogs / History (user) --------
93
+ elif item["cmd"] == "tg_contacts_list":
94
+ response = self.cmd_tg_contacts_list(item)
95
+ elif item["cmd"] == "tg_dialogs_list":
96
+ response = self.cmd_tg_dialogs_list(item)
97
+ elif item["cmd"] == "tg_messages_get":
98
+ response = self.cmd_tg_messages_get(item)
99
+
100
+ if response:
101
+ responses.append(response)
102
+
103
+ except Exception as e:
104
+ responses.append(self.make_response(item, self.throw_error(e)))
105
+
106
+ if responses:
107
+ self.reply_more(responses)
108
+ if self.msg is not None:
109
+ self.status(self.msg)
110
+ except Exception as e:
111
+ self.error(e)
112
+ finally:
113
+ self.cleanup()
114
+
115
+ # ---------------------- Common helpers ----------------------
116
+
117
+ def _mode(self) -> str:
118
+ m = (self.plugin.get_option_value("mode") or "bot").strip().lower()
119
+ return "user" if m == "user" else "bot"
120
+
121
+ def _timeout(self) -> int:
122
+ try:
123
+ return int(self.plugin.get_option_value("http_timeout") or 30)
124
+ except Exception:
125
+ return 30
126
+
127
+ def _api_base(self) -> str:
128
+ return (self.plugin.get_option_value("api_base") or "https://api.telegram.org").rstrip("/")
129
+
130
+ def _guess_mime(self, path: str) -> str:
131
+ mt, _ = mimetypes.guess_type(path)
132
+ return mt or "application/octet-stream"
133
+
134
+ def prepare_path(self, path: str) -> str:
135
+ if path in [".", "./"]:
136
+ return self.plugin.window.core.config.get_user_dir("data")
137
+ if self.is_absolute_path(path):
138
+ return path
139
+ return os.path.join(self.plugin.window.core.config.get_user_dir("data"), path)
140
+
141
+ def is_absolute_path(self, path: str) -> bool:
142
+ return os.path.isabs(path)
143
+
144
+ # ---------------------- Bot API helpers ----------------------
145
+
146
+ def _bot_token(self) -> str:
147
+ tok = (self.plugin.get_option_value("bot_token") or "").strip()
148
+ if not tok:
149
+ raise RuntimeError("Telegram bot_token is missing (set it in options or switch mode to 'user').")
150
+ return tok
151
+
152
+ def _bot_url(self, method: str) -> str:
153
+ return f"{self._api_base()}/bot{self._bot_token()}/{method}"
154
+
155
+ def _bot_file_url(self, file_path: str) -> str:
156
+ return f"{self._api_base()}/file/bot{self._bot_token()}/{file_path}"
157
+
158
+ def _bot_handle(self, r: requests.Response) -> dict:
159
+ try:
160
+ data = r.json() if r.content else {}
161
+ except Exception:
162
+ data = {"raw": r.text}
163
+ if r.status_code != 200 or not data.get("ok"):
164
+ desc = (data.get("description") or r.text)
165
+ raise RuntimeError(f"Telegram Bot API error {r.status_code}: {desc}")
166
+ return data["result"]
167
+
168
+ def _bot_get(self, method: str, params: dict = None) -> dict:
169
+ r = requests.get(self._bot_url(method), params=params or {}, timeout=self._timeout())
170
+ return self._bot_handle(r)
171
+
172
+ def _bot_post(self, method: str, data: dict = None, files: dict | None = None) -> dict:
173
+ r = requests.post(self._bot_url(method), data=data or {}, files=files, timeout=self._timeout())
174
+ return self._bot_handle(r)
175
+
176
+ # ---------------------- Telethon (user mode) ----------------------
177
+
178
+ def _ensure_telethon(self):
179
+ try:
180
+ import telethon # noqa
181
+ except Exception:
182
+ raise RuntimeError("Telethon not installed. Install: pip install telethon")
183
+
184
+ def _tl_get_client(self, need_auth: bool = False):
185
+ """
186
+ Returns connected Telethon client (creates on demand).
187
+ """
188
+ self._ensure_telethon()
189
+ from telethon.sessions import StringSession
190
+ from telethon.sync import TelegramClient
191
+
192
+ api_id = self.plugin.get_option_value("api_id")
193
+ api_hash = self.plugin.get_option_value("api_hash")
194
+ if not (api_id and api_hash):
195
+ raise RuntimeError("api_id/api_hash required for user mode (set options).")
196
+
197
+ session_str = (self.plugin.get_option_value("user_session") or "").strip()
198
+ sess = StringSession(session_str or None)
199
+
200
+ with self._tl_lock:
201
+ if self._tl_client is None:
202
+ self._tl_client = TelegramClient(sess, int(api_id), api_hash)
203
+ self._tl_client.connect()
204
+ else:
205
+ if not self._tl_client.is_connected():
206
+ self._tl_client.connect()
207
+
208
+ authed = bool(self._tl_client.is_user_authorized())
209
+ if not authed and need_auth:
210
+ # Optional auto-begin: send code when phone is available
211
+ phone = (self.plugin.get_option_value("phone_number") or "").strip()
212
+ if phone and bool(self.plugin.get_option_value("auto_login_begin") or True):
213
+ try:
214
+ self._tl_client.send_code_request(phone)
215
+ self.msg = f"Telegram (user): code sent to {phone}. Now run tg_login_complete with 'code'."
216
+ except Exception as e:
217
+ raise RuntimeError(f"Failed to send login code: {e}")
218
+ raise RuntimeError("Not authorized in user mode. Run tg_login_begin then tg_login_complete.")
219
+ return self._tl_client
220
+
221
+ def _tl_save_session(self):
222
+ # Save StringSession to options
223
+ from telethon.sessions import StringSession
224
+ s = self._tl_client.session.save()
225
+ self.plugin.set_option_value("user_session", s)
226
+
227
+ def _tl_resolve(self, client, chat: str | int):
228
+ # Resolves @username, phone, or numeric id/entity
229
+ if isinstance(chat, int):
230
+ return chat
231
+ chat = (chat or "").strip()
232
+ if not chat:
233
+ raise RuntimeError("Param 'chat' required.")
234
+ return client.get_entity(chat)
235
+
236
+ # ---------------------- Commands: Auth (user) ----------------------
237
+
238
+ def cmd_tg_login_begin(self, item: dict) -> dict:
239
+ self._ensure_telethon()
240
+ p = item.get("params", {}) or {}
241
+ phone = (p.get("phone") or self.plugin.get_option_value("phone_number") or "").strip()
242
+ if not phone:
243
+ return self.make_response(item, "Param 'phone' required or set 'phone_number' in options.")
244
+ client = self._tl_get_client(need_auth=False)
245
+ try:
246
+ client.send_code_request(phone)
247
+ return self.make_response(item, f"Code sent to {phone}. Next call tg_login_complete with 'code'.")
248
+ except Exception as e:
249
+ return self.make_response(item, self.throw_error(e))
250
+
251
+ def cmd_tg_login_complete(self, item: dict) -> dict:
252
+ self._ensure_telethon()
253
+ from telethon.errors import SessionPasswordNeededError
254
+ p = item.get("params", {}) or {}
255
+ code = (p.get("code") or "").strip()
256
+ phone = (p.get("phone") or self.plugin.get_option_value("phone_number") or "").strip()
257
+ password = p.get("password") # optional 2FA
258
+
259
+ if not (phone and code):
260
+ return self.make_response(item, "Params 'phone' and 'code' required (phone can be saved in options).")
261
+
262
+ client = self._tl_get_client(need_auth=False)
263
+ try:
264
+ client.sign_in(phone=phone, code=code)
265
+ except SessionPasswordNeededError:
266
+ if not password and not (self.plugin.get_option_value("password_2fa") or ""):
267
+ return self.make_response(item, "2FA password required. Provide 'password' or set 'password_2fa' in options.")
268
+ client.sign_in(password=password or self.plugin.get_option_value("password_2fa"))
269
+
270
+ # Save session for reuse
271
+ self._tl_save_session()
272
+
273
+ # Cache identity
274
+ me = client.get_me()
275
+ data = {
276
+ "id": me.id,
277
+ "username": getattr(me, "username", None),
278
+ "first_name": getattr(me, "first_name", None),
279
+ "phone": getattr(me, "phone", None),
280
+ "is_bot": bool(getattr(me, "bot", False)),
281
+ }
282
+ return self.make_response(item, {"authorized": True, "me": data})
283
+
284
+ def cmd_tg_logout(self, item: dict) -> dict:
285
+ self._ensure_telethon()
286
+ client = self._tl_get_client(need_auth=False)
287
+ try:
288
+ client.log_out()
289
+ except Exception:
290
+ pass
291
+ try:
292
+ client.disconnect()
293
+ except Exception:
294
+ pass
295
+ self._tl_client = None
296
+ self.plugin.set_option_value("user_session", "")
297
+ return self.make_response(item, {"authorized": False})
298
+
299
+ # ---------------------- Commands: Info ----------------------
300
+
301
+ def cmd_tg_mode(self, item: dict) -> dict:
302
+ return self.make_response(item, {"mode": self._mode()})
303
+
304
+ def cmd_tg_me(self, item: dict) -> dict:
305
+ if self._mode() == "bot":
306
+ res = self._bot_get("getMe")
307
+ return self.make_response(item, res)
308
+ else:
309
+ client = self._tl_get_client(need_auth=True)
310
+ me = client.get_me()
311
+ data = {
312
+ "id": me.id,
313
+ "username": getattr(me, "username", None),
314
+ "first_name": getattr(me, "first_name", None),
315
+ "last_name": getattr(me, "last_name", None),
316
+ "phone": getattr(me, "phone", None),
317
+ "is_bot": bool(getattr(me, "bot", False)),
318
+ }
319
+ return self.make_response(item, data)
320
+
321
+ # ---------------------- Commands: Messaging ----------------------
322
+
323
+ def _default_send_flags(self, p: dict) -> dict:
324
+ flags = {}
325
+ if p.get("parse_mode") or self.plugin.get_option_value("default_parse_mode"):
326
+ flags["parse_mode"] = p.get("parse_mode") or self.plugin.get_option_value("default_parse_mode")
327
+ if p.get("disable_web_page_preview") is not None:
328
+ flags["disable_web_page_preview"] = bool(p.get("disable_web_page_preview"))
329
+ else:
330
+ if self.plugin.get_option_value("default_disable_preview"):
331
+ flags["disable_web_page_preview"] = True
332
+ if p.get("disable_notification") is not None:
333
+ flags["disable_notification"] = bool(p.get("disable_notification"))
334
+ else:
335
+ if self.plugin.get_option_value("default_disable_notification"):
336
+ flags["disable_notification"] = True
337
+ if p.get("protect_content") is not None:
338
+ flags["protect_content"] = bool(p.get("protect_content"))
339
+ else:
340
+ if self.plugin.get_option_value("default_protect_content"):
341
+ flags["protect_content"] = True
342
+ if p.get("reply_to_message_id"):
343
+ flags["reply_to_message_id"] = int(p.get("reply_to_message_id"))
344
+ return flags
345
+
346
+ def cmd_tg_send_message(self, item: dict) -> dict:
347
+ p = item.get("params", {}) or {}
348
+ chat = p.get("chat")
349
+ text = p.get("text", "")
350
+ if not (chat and text):
351
+ return self.make_response(item, "Params 'chat' and 'text' required.")
352
+
353
+ if self._mode() == "bot":
354
+ data = {"chat_id": chat, "text": text}
355
+ data.update(self._default_send_flags(p))
356
+ res = self._bot_post("sendMessage", data=data)
357
+ return self.make_response(item, res)
358
+ else:
359
+ client = self._tl_get_client(need_auth=True)
360
+ entity = self._tl_resolve(client, chat)
361
+ parse_mode = (p.get("parse_mode") or self.plugin.get_option_value("default_parse_mode") or None)
362
+ # Telethon accepts "html" or "markdown"
363
+ if parse_mode and parse_mode.upper() == "MARKDOWNV2":
364
+ parse_mode = "markdown"
365
+ msg = client.send_message(entity, text, parse_mode=(parse_mode.lower() if parse_mode else None), link_preview=not bool(p.get("disable_web_page_preview") or self.plugin.get_option_value("default_disable_preview")))
366
+ return self.make_response(item, {"id": msg.id, "date": msg.date.isoformat()})
367
+
368
+ def cmd_tg_send_photo(self, item: dict) -> dict:
369
+ p = item.get("params", {}) or {}
370
+ chat = p.get("chat")
371
+ photo = p.get("photo") or p.get("path")
372
+ caption = p.get("caption", "")
373
+ if not (chat and photo):
374
+ return self.make_response(item, "Params 'chat' and 'photo' (or 'path') required.")
375
+
376
+ if self._mode() == "bot":
377
+ data = {"chat_id": chat, "caption": caption}
378
+ data.update(self._default_send_flags(p))
379
+ files = None
380
+ # Accept local path, URL or file_id
381
+ if os.path.exists(self.prepare_path(photo)):
382
+ local = self.prepare_path(photo)
383
+ files = {"photo": (os.path.basename(local), open(local, "rb"), self._guess_mime(local))}
384
+ else:
385
+ data["photo"] = photo
386
+ res = self._bot_post("sendPhoto", data=data, files=files)
387
+ return self.make_response(item, res)
388
+ else:
389
+ client = self._tl_get_client(need_auth=True)
390
+ entity = self._tl_resolve(client, chat)
391
+ local = self.prepare_path(photo) if not photo.lower().startswith("http") else photo
392
+ msg = client.send_file(entity, local, caption=caption)
393
+ return self.make_response(item, {"id": msg.id, "date": msg.date.isoformat()})
394
+
395
+ def cmd_tg_send_document(self, item: dict) -> dict:
396
+ p = item.get("params", {}) or {}
397
+ chat = p.get("chat")
398
+ doc = p.get("document") or p.get("path")
399
+ caption = p.get("caption", "")
400
+ if not (chat and doc):
401
+ return self.make_response(item, "Params 'chat' and 'document' (or 'path') required.")
402
+
403
+ if self._mode() == "bot":
404
+ data = {"chat_id": chat, "caption": caption}
405
+ data.update(self._default_send_flags(p))
406
+ files = None
407
+ if os.path.exists(self.prepare_path(doc)):
408
+ local = self.prepare_path(doc)
409
+ files = {"document": (os.path.basename(local), open(local, "rb"), self._guess_mime(local))}
410
+ else:
411
+ data["document"] = doc
412
+ res = self._bot_post("sendDocument", data=data, files=files)
413
+ return self.make_response(item, res)
414
+ else:
415
+ client = self._tl_get_client(need_auth=True)
416
+ entity = self._tl_resolve(client, chat)
417
+ local = self.prepare_path(doc) if not str(doc).lower().startswith("http") else doc
418
+ msg = client.send_file(entity, local, caption=caption, force_document=True)
419
+ return self.make_response(item, {"id": msg.id, "date": msg.date.isoformat()})
420
+
421
+ # ---------------------- Commands: Chats ----------------------
422
+
423
+ def cmd_tg_get_chat(self, item: dict) -> dict:
424
+ p = item.get("params", {}) or {}
425
+ chat = p.get("chat")
426
+ if not chat:
427
+ return self.make_response(item, "Param 'chat' required (id or @username).")
428
+
429
+ if self._mode() == "bot":
430
+ res = self._bot_get("getChat", params={"chat_id": chat})
431
+ return self.make_response(item, res)
432
+ else:
433
+ client = self._tl_get_client(need_auth=True)
434
+ ent = self._tl_resolve(client, chat)
435
+ data = {
436
+ "id": getattr(ent, "id", None),
437
+ "title": getattr(ent, "title", None) or f"{getattr(ent, 'first_name', '')} {getattr(ent, 'last_name', '')}".strip(),
438
+ "username": getattr(ent, "username", None),
439
+ "is_channel": getattr(ent, "__class__", type("x",(object,),{})).__name__.lower().find("channel") >= 0,
440
+ }
441
+ return self.make_response(item, data)
442
+
443
+ # ---------------------- Commands: Updates / Files (bot) ----------------------
444
+
445
+ def cmd_tg_get_updates(self, item: dict) -> dict:
446
+ if self._mode() != "bot":
447
+ return self.make_response(item, "tg_get_updates is available in bot mode only.")
448
+ p = item.get("params", {}) or {}
449
+ offset = p.get("offset")
450
+ if offset is None:
451
+ # continue from last stored
452
+ last = self.plugin.get_option_value("last_update_id")
453
+ if last:
454
+ try:
455
+ offset = int(last) + 1
456
+ except Exception:
457
+ offset = None
458
+ params = {
459
+ "timeout": int(p.get("timeout") or 0),
460
+ }
461
+ if offset is not None:
462
+ params["offset"] = int(offset)
463
+ if p.get("allowed_updates"):
464
+ params["allowed_updates"] = json.dumps(p.get("allowed_updates"))
465
+ res = self._bot_get("getUpdates", params=params)
466
+ if res:
467
+ max_id = max(u.get("update_id", 0) for u in res)
468
+ self.plugin.set_option_value("last_update_id", str(max_id))
469
+ return self.make_response(item, res or [])
470
+
471
+ def cmd_tg_download_file(self, item: dict) -> dict:
472
+ if self._mode() != "bot":
473
+ return self.make_response(item, "tg_download_file is available in bot mode only.")
474
+ p = item.get("params", {}) or {}
475
+ file_id = p.get("file_id")
476
+ save_as = p.get("save_as") # relative to data dir if not absolute
477
+ if not file_id:
478
+ return self.make_response(item, "Param 'file_id' required.")
479
+ info = self._bot_post("getFile", data={"file_id": file_id})
480
+ file_path = info.get("file_path")
481
+ if not file_path:
482
+ return self.make_response(item, "No file_path returned by getFile.")
483
+ url = self._bot_file_url(file_path)
484
+ r = requests.get(url, timeout=self._timeout())
485
+ if r.status_code != 200:
486
+ return self.make_response(item, f"Download error: HTTP {r.status_code}")
487
+ local = self.prepare_path(save_as or os.path.basename(file_path))
488
+ os.makedirs(os.path.dirname(local), exist_ok=True)
489
+ with open(local, "wb") as fh:
490
+ fh.write(r.content)
491
+ return self.make_response(item, {"saved_path": local, "size": len(r.content)})
492
+
493
+ # ---------------------- Commands: Contacts / Dialogs / Messages (user) ----------------------
494
+
495
+ def cmd_tg_contacts_list(self, item: dict) -> dict:
496
+ if self._mode() != "user":
497
+ return self.make_response(item, "tg_contacts_list is available in user mode only.")
498
+ client = self._tl_get_client(need_auth=True)
499
+ contacts = client.get_contacts()
500
+ out = []
501
+ for u in contacts:
502
+ out.append({
503
+ "id": u.id,
504
+ "first_name": getattr(u, "first_name", None),
505
+ "last_name": getattr(u, "last_name", None),
506
+ "username": getattr(u, "username", None),
507
+ "phone": getattr(u, "phone", None),
508
+ })
509
+ return self.make_response(item, {"count": len(out), "contacts": out})
510
+
511
+ def cmd_tg_dialogs_list(self, item: dict) -> dict:
512
+ if self._mode() != "user":
513
+ return self.make_response(item, "tg_dialogs_list is available in user mode only.")
514
+ p = item.get("params", {}) or {}
515
+ limit = int(p.get("limit") or 20)
516
+ client = self._tl_get_client(need_auth=True)
517
+ dialogs = client.get_dialogs(limit=limit)
518
+ out = []
519
+ for d in dialogs:
520
+ ent = d.entity
521
+ title = getattr(ent, "title", None) or f"{getattr(ent, 'first_name', '')} {getattr(ent, 'last_name', '')}".strip()
522
+ out.append({
523
+ "id": getattr(ent, "id", None),
524
+ "title": title,
525
+ "username": getattr(ent, "username", None),
526
+ "unread_count": getattr(d, "unread_count", 0),
527
+ "is_channel": "Channel" in ent.__class__.__name__,
528
+ })
529
+ return self.make_response(item, {"count": len(out), "dialogs": out})
530
+
531
+ def cmd_tg_messages_get(self, item: dict) -> dict:
532
+ if self._mode() != "user":
533
+ return self.make_response(item, "tg_messages_get is available in user mode only.")
534
+ p = item.get("params", {}) or {}
535
+ chat = p.get("chat")
536
+ limit = int(p.get("limit") or 30)
537
+ if not chat:
538
+ return self.make_response(item, "Param 'chat' required.")
539
+ client = self._tl_get_client(need_auth=True)
540
+ ent = self._tl_resolve(client, chat)
541
+
542
+ kwargs = {}
543
+ if p.get("offset_id"):
544
+ kwargs["offset_id"] = int(p["offset_id"])
545
+ if p.get("min_id"):
546
+ kwargs["min_id"] = int(p["min_id"])
547
+ if p.get("max_id"):
548
+ kwargs["max_id"] = int(p["max_id"])
549
+ if p.get("search"):
550
+ kwargs["search"] = p.get("search")
551
+
552
+ msgs = client.get_messages(ent, limit=limit, **kwargs)
553
+ out = []
554
+ for m in msgs:
555
+ out.append({
556
+ "id": m.id,
557
+ "date": m.date.isoformat() if m.date else None,
558
+ "text": m.message or "",
559
+ "sender_id": getattr(m, "sender_id", None),
560
+ "reply_to": getattr(m, "reply_to_msg_id", None),
561
+ "media": bool(m.media),
562
+ })
563
+ return self.make_response(item, {"count": len(out), "messages": out})
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.06.30 02:00:00 #
10
+ # ================================================== #
11
+
12
+ from .plugin import *