wintergram-userbot 1.1.2__tar.gz → 1.2__tar.gz

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. wintergram_userbot-1.2/MANIFEST.in +4 -0
  2. {wintergram_userbot-1.1.2/wintergram_userbot.egg-info → wintergram_userbot-1.2}/PKG-INFO +6 -23
  3. wintergram_userbot-1.2/pyproject.toml +26 -0
  4. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/__init__.py +1 -1
  5. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/api/functions.py +402 -9
  6. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/client.py +281 -3
  7. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2/wintergram_userbot.egg-info}/PKG-INFO +6 -23
  8. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot.egg-info/SOURCES.txt +0 -1
  9. wintergram_userbot-1.2/wintergram_userbot.egg-info/requires.txt +4 -0
  10. wintergram_userbot-1.1.2/LICENSE +0 -21
  11. wintergram_userbot-1.1.2/MANIFEST.in +0 -17
  12. wintergram_userbot-1.1.2/pyproject.toml +0 -43
  13. wintergram_userbot-1.1.2/wintergram_userbot.egg-info/requires.txt +0 -1
  14. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/README.md +0 -0
  15. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/setup.cfg +0 -0
  16. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/api/__init__.py +0 -0
  17. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/api/updates.py +0 -0
  18. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/crypto/__init__.py +0 -0
  19. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/crypto/aes.py +0 -0
  20. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/crypto/auth_key.py +0 -0
  21. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/crypto/dh.py +0 -0
  22. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/crypto/rsa.py +0 -0
  23. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/crypto/srp.py +0 -0
  24. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/errors.py +0 -0
  25. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/network/__init__.py +0 -0
  26. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/network/connection.py +0 -0
  27. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/network/mtproto.py +0 -0
  28. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/network/transport.py +0 -0
  29. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/session/__init__.py +0 -0
  30. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/session/session.py +0 -0
  31. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/session/storage.py +0 -0
  32. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/__init__.py +0 -0
  33. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/schema_resolver.py +0 -0
  34. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/schemas/__init__.py +0 -0
  35. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/schemas/data/core.json +0 -0
  36. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/schemas/data/latest.json +0 -0
  37. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/schemas/data/v113.json +0 -0
  38. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/schemas/data/v119.json +0 -0
  39. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/schemas/data/v128.json +0 -0
  40. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/schemas/data/v133.json +0 -0
  41. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/serializer.py +0 -0
  42. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot/tl/types.py +0 -0
  43. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot.egg-info/dependency_links.txt +0 -0
  44. {wintergram_userbot-1.1.2 → wintergram_userbot-1.2}/wintergram_userbot.egg-info/top_level.txt +0 -0
@@ -0,0 +1,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include wintergram_userbot *.py
4
+ recursive-include wintergram_userbot *.json
@@ -1,30 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wintergram-userbot
3
- Version: 1.1.2
4
- Summary: Полнофункциональный асинхронный MTProto-клиент для приватных Telegram-серверов (layers 167-228) с поддержкой отправки медиа
5
- Author-email: femboychik <your.email@example.com>
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/femboychik/wintergram-userbot
8
- Project-URL: Repository, https://github.com/femboychik/wintergram-userbot
9
- Project-URL: Issues, https://github.com/femboychik/wintergram-userbot/issues
10
- Keywords: telegram,mtproto,userbot,async,client
11
- Classifier: Development Status :: 4 - Beta
12
- Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: MIT License
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Operating System :: OS Independent
21
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
- Classifier: Topic :: Communications :: Chat
3
+ Version: 1.2
4
+ Summary: WinterGram Userbot for custom MTProto server
5
+ Author: WinterGram Team
23
6
  Requires-Python: >=3.8
24
7
  Description-Content-Type: text/markdown
25
- License-File: LICENSE
26
- Requires-Dist: aiohttp>=3.8.0
27
- Dynamic: license-file
8
+ Requires-Dist: pycryptodome>=3.19.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
28
11
 
29
12
  # wintergram-userbot
30
13
 
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=45", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wintergram-userbot"
7
+ version = "1.2"
8
+ description = "WinterGram Userbot for custom MTProto server"
9
+ readme = "README.md"
10
+ authors = [{name = "WinterGram Team"}]
11
+ requires-python = ">=3.8"
12
+ dependencies = [
13
+ "pycryptodome>=3.19.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ dev = [
18
+ "pytest>=7.0.0",
19
+ ]
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["."]
23
+ include = ["wintergram_userbot*"]
24
+
25
+ [tool.setuptools.package-data]
26
+ wintergram_userbot = ["tl/schemas/data/*.json"]
@@ -4,7 +4,7 @@
4
4
  Поддерживаются слои (layers) от 167 до 228 включительно.
5
5
  """
6
6
 
7
- __version__ = "1.1.2"
7
+ __version__ = "1.2"
8
8
 
9
9
  from .client import WinterGramClient
10
10
  from .errors import (
@@ -187,6 +187,32 @@ class Functions:
187
187
  # отправка / история
188
188
  # ------------------------------------------------------------------ #
189
189
 
190
+ # ------------------------------------------------------------------ #
191
+ # ответы на сообщения (разные эры схемы)
192
+ # ------------------------------------------------------------------ #
193
+ def reply_params(self, method: str, reply_to):
194
+ """Параметры ответа на сообщение для метода - с учётом эры слоя.
195
+
196
+ Старые слои ждут int в reply_to_msg_id, новые - объект
197
+ inputReplyToMessage. Возвращает то, что нужно добавить в params.
198
+ """
199
+ if not reply_to:
200
+ return {}
201
+ if not isinstance(reply_to, int):
202
+ return {"reply_to": reply_to}
203
+
204
+ out = {}
205
+ names = self._param_names(method)
206
+ if "reply_to_msg_id" in names:
207
+ out["reply_to_msg_id"] = reply_to
208
+ if "reply_to" in names:
209
+ reply_desc = self.schema.by_name("inputReplyToMessage")
210
+ if reply_desc is None:
211
+ out["reply_to_msg_id"] = reply_to
212
+ else:
213
+ out["reply_to"] = {"_id": reply_desc["id"], "reply_to_msg_id": reply_to}
214
+ return out
215
+
190
216
  async def send_message(self, peer, text: str, random_id=None, reply_to=None, reply_markup=None):
191
217
  """messages.sendMessage(peer, text, random_id, reply_to, reply_markup)."""
192
218
  params = {
@@ -196,15 +222,8 @@ class Functions:
196
222
  int.from_bytes(os.urandom(8), "little", signed=True)
197
223
  ),
198
224
  }
199
- if reply_to is not None:
200
- # разные эры: reply_to_msg_id (int) или reply_to (InputReplyTo)
201
- if "reply_to_msg_id" in self._param_names("messages.sendMessage"):
202
- if isinstance(reply_to, int):
203
- params["reply_to_msg_id"] = reply_to
204
- if "reply_to" in self._param_names("messages.sendMessage"):
205
- if isinstance(reply_to, int):
206
- reply_desc = self.schema.by_name("inputReplyToMessage")
207
- params["reply_to"] = {"_id": reply_desc["id"], "reply_to_msg_id": reply_to}
225
+ # разные эры: reply_to_msg_id (int) или reply_to (InputReplyTo)
226
+ params.update(self.reply_params("messages.sendMessage", reply_to))
208
227
  if reply_markup is not None:
209
228
  params["reply_markup"] = reply_markup
210
229
  return await self.conn.rpc("messages.sendMessage", params)
@@ -384,6 +403,240 @@ class Functions:
384
403
  params = {"offline": offline}
385
404
  return await self.conn.rpc("account.updateStatus", params)
386
405
 
406
+ # ------------------------------------------------------------------ #
407
+ # профиль: юзернеймы, аватар, приватность, сессии
408
+ # ------------------------------------------------------------------ #
409
+
410
+ _PRIVACY_KEYS = {
411
+ "status_timestamp": "inputPrivacyKeyStatusTimestamp",
412
+ "last_seen": "inputPrivacyKeyStatusTimestamp",
413
+ "phone_call": "inputPrivacyKeyPhoneCall",
414
+ "phone_p2p": "inputPrivacyKeyPhoneP2P",
415
+ "chat_invite": "inputPrivacyKeyChatInvite",
416
+ "phone_number": "inputPrivacyKeyPhoneNumber",
417
+ "profile_photo": "inputPrivacyKeyProfilePhoto",
418
+ "forwards": "inputPrivacyKeyForwards",
419
+ "about": "inputPrivacyKeyAbout",
420
+ "voice_messages": "inputPrivacyKeyVoiceMessages",
421
+ }
422
+
423
+ _PRIVACY_VALUES = {
424
+ "all": "inputPrivacyValueAllowAll",
425
+ "everyone": "inputPrivacyValueAllowAll",
426
+ "contacts": "inputPrivacyValueAllowContacts",
427
+ "close_friends": "inputPrivacyValueAllowCloseFriends",
428
+ "bots": "inputPrivacyValueAllowBots",
429
+ "premium": "inputPrivacyValueAllowPremium",
430
+ "nobody": "inputPrivacyValueAllowUsers",
431
+ "chats": "inputPrivacyValueAllowChatParticipants",
432
+ }
433
+
434
+ def _construct(self, ctor, **fields):
435
+ """Собирает конструктор текущей эры по имени; fields — его поля."""
436
+ desc = self.schema.by_name(ctor)
437
+ if desc is None:
438
+ raise RPCError(0, "%s not available on layer %d" % (ctor, self.schema.layer))
439
+ obj = {"_id": desc["id"]}
440
+ obj.update(fields)
441
+ return obj
442
+
443
+ # -------------------------------------------------------------- #
444
+ # юзернеймы
445
+ # -------------------------------------------------------------- #
446
+
447
+ async def check_username(self, username: str):
448
+ """account.checkUsername - свободен ли юзернейм (True/False)."""
449
+ return await self.conn.rpc("account.checkUsername", {"username": username})
450
+
451
+ async def update_username(self, username: str):
452
+ """account.updateUsername - ставит основной юзернейм.
453
+
454
+ Пустая строка снимает текущий юзернейм.
455
+ """
456
+ return await self.conn.rpc("account.updateUsername", {"username": username})
457
+
458
+ async def toggle_username(self, username: str, active=True):
459
+ """account.toggleUsername - вкл/выкл дополнительный юзернейм."""
460
+ return await self.conn.rpc("account.toggleUsername", {
461
+ "username": username,
462
+ "active": bool(active),
463
+ })
464
+
465
+ async def reorder_usernames(self, order):
466
+ """account.reorderUsernames - первый в списке становится основным."""
467
+ return await self.conn.rpc("account.reorderUsernames", {"order": list(order)})
468
+
469
+ # -------------------------------------------------------------- #
470
+ # аватар
471
+ # -------------------------------------------------------------- #
472
+
473
+ async def upload_file(self, data: bytes, file_name="file", part_size=524288):
474
+ """Заливает байты по кускам через upload.saveFilePart.
475
+
476
+ Returns:
477
+ inputFile текущей эры - готов к передаче в photos.* / messages.sendMedia.
478
+ """
479
+ if not isinstance(data, (bytes, bytearray)):
480
+ raise TypeError("upload_file expects bytes, got %r" % type(data).__name__)
481
+ chunks = [bytes(data[i:i + part_size]) for i in range(0, len(data), part_size)] or [b""]
482
+ file_id = int.from_bytes(os.urandom(8), "little", signed=True)
483
+ for idx, chunk in enumerate(chunks):
484
+ await self.conn.rpc("upload.saveFilePart", {
485
+ "file_id": file_id,
486
+ "file_part": idx,
487
+ "bytes": chunk,
488
+ })
489
+ return self._construct(
490
+ "inputFile", id=file_id, parts=len(chunks), name=file_name, md5_checksum="",
491
+ )
492
+
493
+ async def upload_profile_photo(self, data: bytes, file_name="avatar.jpg", fallback=None):
494
+ """photos.uploadProfilePhoto - ставит фото основной аватаркой.
495
+
496
+ Args:
497
+ data: байты изображения
498
+ file_name: имя файла (для сервера)
499
+ fallback: True - запасная аватарка (если сервер/слой поддерживает)
500
+ """
501
+ input_file = await self.upload_file(data, file_name=file_name)
502
+ params = {"file": input_file}
503
+ if fallback is not None:
504
+ params["fallback"] = bool(fallback)
505
+ return await self.conn.rpc("photos.uploadProfilePhoto", params)
506
+
507
+ async def delete_profile_photo(self, photo_id):
508
+ """photos.deletePhotos - удаляет аватарки по id (списком или по одному)."""
509
+ ids = photo_id if isinstance(photo_id, (list, tuple)) else [photo_id]
510
+ payload = [self._construct("inputPhoto", id=int(i)) for i in ids]
511
+ return await self.conn.rpc("photos.deletePhotos", {"id": payload})
512
+
513
+ async def get_profile_photos(self, user_id=None, offset=0, max_id=0, limit=20):
514
+ """photos.getUserPhotos - аватарки пользователя (свои, если user_id не задан)."""
515
+ return await self.conn.rpc("photos.getUserPhotos", {
516
+ "user_id": user_id if user_id is not None else self.input_user_self(),
517
+ "offset": int(offset),
518
+ "max_id": int(max_id),
519
+ "limit": int(limit),
520
+ })
521
+
522
+ # -------------------------------------------------------------- #
523
+ # приватность
524
+ # -------------------------------------------------------------- #
525
+
526
+ def privacy_key(self, key):
527
+ """Имя ключа ('last_seen', 'phone_number', ...) -> InputPrivacyKey."""
528
+ if isinstance(key, dict):
529
+ return key
530
+ name = self._PRIVACY_KEYS.get(str(key).lower())
531
+ if name is None:
532
+ raise ValueError("unknown privacy key %r; known: %s"
533
+ % (key, ", ".join(sorted(self._PRIVACY_KEYS))))
534
+ return self._construct(name)
535
+
536
+ def privacy_rule(self, rule, chats=None):
537
+ """Имя правила ('all', 'contacts', 'nobody', ...) -> InputPrivacyRule.
538
+
539
+ Для 'chats' передай chats=[InputChat,...].
540
+ """
541
+ if isinstance(rule, dict):
542
+ return rule
543
+ name = self._PRIVACY_VALUES.get(str(rule).lower())
544
+ if name is None:
545
+ raise ValueError("unknown privacy rule %r; known: %s"
546
+ % (rule, ", ".join(sorted(self._PRIVACY_VALUES))))
547
+ if name == "inputPrivacyValueAllowChatParticipants":
548
+ return self._construct(name, chats=list(chats or []))
549
+ return self._construct(name)
550
+
551
+ async def get_privacy(self, key):
552
+ """account.getPrivacy - текущие правила приватности."""
553
+ return await self.conn.rpc("account.getPrivacy", {"key": self.privacy_key(key)})
554
+
555
+ async def set_privacy(self, key, rules):
556
+ """account.setPrivacy - правила приватности.
557
+
558
+ Args:
559
+ key: ключ ('last_seen', 'profile_photo', ...)
560
+ rules: список правил: ['nobody'], ['contacts'], ['chats'] и т.п.
561
+ Для конкретных чатов - пара ('chats', [InputChat, ...])
562
+ """
563
+ built = []
564
+ for rule in rules:
565
+ if isinstance(rule, (list, tuple)):
566
+ if not rule:
567
+ raise ValueError("empty privacy rule")
568
+ built.append(self.privacy_rule(rule[0], chats=rule[1] if len(rule) > 1 else None))
569
+ else:
570
+ built.append(self.privacy_rule(rule))
571
+ return await self.conn.rpc("account.setPrivacy", {
572
+ "key": self.privacy_key(key),
573
+ "rules": built,
574
+ })
575
+
576
+ # -------------------------------------------------------------- #
577
+ # оформление профиля
578
+ # -------------------------------------------------------------- #
579
+
580
+ async def update_birthday(self, day, month, year=None):
581
+ """account.updateBirthday - день рождения (layer >= latest only)."""
582
+ return await self.conn.rpc("account.updateBirthday", {
583
+ "birthday": self._construct("birthday", day=int(day), month=int(month), year=int(year or 0)),
584
+ })
585
+
586
+ async def update_profile_color(self, color=None, for_profile=True):
587
+ """account.updateColor - цвет профиля/градиент аватарки."""
588
+ return await self.conn.rpc("account.updateColor", {
589
+ "for_profile": bool(for_profile),
590
+ "color": self._construct("peerColor", color=int(color) if color is not None else 0),
591
+ })
592
+
593
+ async def set_emoji_status(self, emoji=None):
594
+ """account.updateEmojiStatus - эмодзи-статус.
595
+
596
+ emoji=None очищает статус. Установка конкретного эмодзи требует
597
+ конструктор emojiStatusEmoji, которого нет в собранных схемах.
598
+ """
599
+ if emoji is None:
600
+ return await self.conn.rpc("account.updateEmojiStatus", {
601
+ "emoji_status": self._construct("emojiStatusEmpty"),
602
+ })
603
+ desc = self.schema.by_name("emojiStatusEmoji")
604
+ if desc is None:
605
+ raise RPCError(0, "emojiStatusEmoji not available on layer %d; "
606
+ "only clearing is possible" % self.schema.layer)
607
+ document_id = getattr(emoji, "document_id", None)
608
+ return await self.conn.rpc("account.updateEmojiStatus", {
609
+ "emoji_status": self._construct(
610
+ desc["name"], document_id=int(document_id or emoji), until=None,
611
+ ),
612
+ })
613
+
614
+ async def get_recent_emoji_statuses(self, hash=0):
615
+ """account.getRecentEmojiStatuses - недавние эмодзи-статусы."""
616
+ return await self.conn.rpc("account.getRecentEmojiStatuses", {"hash": int(hash)})
617
+
618
+ # -------------------------------------------------------------- #
619
+ # сессии и прочее
620
+ # -------------------------------------------------------------- #
621
+
622
+ async def get_authorizations(self):
623
+ """account.getAuthorizations - список активных сессий аккаунта."""
624
+ return await self.conn.rpc("account.getAuthorizations", {})
625
+
626
+ async def reset_authorization(self, auth_hash):
627
+ """account.resetAuthorization - завершает чужую сессию по hash."""
628
+ return await self.conn.rpc("account.resetAuthorization", {"hash": int(auth_hash)})
629
+
630
+ async def get_account_ttl(self):
631
+ """account.getAccountTTL - настройка автоудаления аккаунта."""
632
+ return await self.conn.rpc("account.getAccountTTL", {})
633
+
634
+ async def set_account_ttl(self, days=0):
635
+ """account.setAccountTTL - автоудаление через N дней (0 — выключить)."""
636
+ return await self.conn.rpc("account.setAccountTTL", {
637
+ "ttl": self._construct("accountDaysTTL", days=int(days)),
638
+ })
639
+
387
640
  # ------------------------------------------------------------------ #
388
641
  # управление чатами
389
642
  # ------------------------------------------------------------------ #
@@ -548,6 +801,146 @@ class Functions:
548
801
 
549
802
  return await self.conn.rpc("messages.setBotCallbackAnswer", params)
550
803
 
804
+ # ------------------------------------------------------------------ #
805
+ # подарки (star gifts)
806
+ # ------------------------------------------------------------------ #
807
+
808
+ async def get_star_gifts(self, hash=0):
809
+ """payments.getStarGifts - получает список доступных подарков.
810
+
811
+ Args:
812
+ hash: хэш для проверки изменений (0 = получить все)
813
+
814
+ Returns:
815
+ Объект payments.StarGifts с полями: gifts, chats, users
816
+ """
817
+ params = {"hash": int(hash)}
818
+ return await self.conn.rpc("payments.getStarGifts", params)
819
+
820
+ async def send_star_gift(self, peer, gift_id, message=None, hide_name=False):
821
+ """Отправляет StarGift пользователю.
822
+
823
+ Args:
824
+ peer: InputPeer получателя подарка
825
+ gift_id: ID подарка (из списка payments.getStarGifts)
826
+ message: текст сообщения к подарку (опционально)
827
+ hide_name: скрыть имя отправителя
828
+
829
+ Returns:
830
+ Результат отправки подарка
831
+ """
832
+ # Создаем InputInvoice для подарка
833
+ invoice_params = {
834
+ "_": "inputInvoiceStarGift",
835
+ "peer": peer,
836
+ "gift_id": int(gift_id),
837
+ "flags": 0
838
+ }
839
+
840
+ if hide_name:
841
+ invoice_params["flags"] |= 1 # flags.0
842
+
843
+ if message:
844
+ invoice_params["flags"] |= 2 # flags.1
845
+ # Создаем TextWithEntities
846
+ invoice_params["message"] = {
847
+ "_": "textWithEntities",
848
+ "text": message,
849
+ "entities": []
850
+ }
851
+
852
+ # Получаем форму оплаты
853
+ _, payment_form = await self.conn.rpc("payments.getPaymentForm", {
854
+ "invoice": invoice_params
855
+ })
856
+
857
+ form_id = payment_form.get("form_id")
858
+ if form_id is None:
859
+ raise RPCError(0, "payment form has no form_id")
860
+
861
+ # Отправляем подарок
862
+ return await self.conn.rpc("payments.sendStarsForm", {
863
+ "form_id": form_id,
864
+ "invoice": invoice_params
865
+ })
866
+
867
+ async def get_saved_star_gifts(self, peer, offset="", limit=100, exclude_unsaved=False):
868
+ """payments.getSavedStarGifts - получает сохраненные подарки.
869
+
870
+ Args:
871
+ peer: InputPeer пользователя
872
+ offset: смещение для пагинации
873
+ limit: количество подарков
874
+ exclude_unsaved: исключить несохраненные
875
+
876
+ Returns:
877
+ Объект payments.SavedStarGifts
878
+ """
879
+ params = {
880
+ "peer": peer,
881
+ "offset": str(offset),
882
+ "limit": int(limit),
883
+ "flags": 0
884
+ }
885
+
886
+ if exclude_unsaved:
887
+ params["flags"] |= 1 # flags.0
888
+
889
+ return await self.conn.rpc("payments.getSavedStarGifts", params)
890
+
891
+ async def convert_star_gift(self, msg_id):
892
+ """payments.convertStarGift - конвертирует подарок в звезды.
893
+
894
+ Args:
895
+ msg_id: ID сообщения с подарком
896
+
897
+ Returns:
898
+ Результат конвертации
899
+ """
900
+ # Создаем InputSavedStarGift
901
+ stargift = {
902
+ "_": "inputSavedStarGiftUser",
903
+ "msg_id": int(msg_id)
904
+ }
905
+
906
+ return await self.conn.rpc("payments.convertStarGift", {
907
+ "stargift": stargift
908
+ })
909
+
910
+ async def transfer_star_gift(self, msg_id, to_peer):
911
+ """Переводит уникальный подарок другому пользователю.
912
+
913
+ Args:
914
+ msg_id: ID сообщения с подарком
915
+ to_peer: InputPeer получателя
916
+
917
+ Returns:
918
+ Результат перевода
919
+ """
920
+ # Создаем InputSavedStarGift
921
+ stargift = {
922
+ "_": "inputSavedStarGiftUser",
923
+ "msg_id": int(msg_id)
924
+ }
925
+
926
+ return await self.conn.rpc("payments.transferStarGift", {
927
+ "stargift": stargift,
928
+ "to_id": to_peer
929
+ })
930
+
931
+ async def check_can_send_gift(self, gift_id):
932
+ """payments.checkCanSendGift - проверяет возможность отправки подарка.
933
+
934
+ Args:
935
+ gift_id: ID подарка
936
+
937
+ Returns:
938
+ payments.CheckCanSendGiftResult (Ok или Fail с причиной)
939
+ """
940
+ return await self.conn.rpc("payments.checkCanSendGift", {
941
+ "gift_id": int(gift_id)
942
+ })
943
+
551
944
  # ------------------------------------------------------------------ #
552
945
  def _param_names(self, method_name):
553
946
  desc = self.schema.by_name(method_name)
@@ -3,6 +3,7 @@
3
3
  import asyncio
4
4
  import inspect
5
5
  import logging
6
+ import os
6
7
 
7
8
  from .errors import (
8
9
  WinterGramError,
@@ -277,13 +278,14 @@ class WinterGramClient:
277
278
  raise RPCError(0, "peer %r could not be resolved" % (peer,))
278
279
  return await self.functions.send_message(peer_obj, text)
279
280
 
280
- async def send_photo(self, peer, photo_path_or_url, caption=""):
281
+ async def send_photo(self, peer, photo_path_or_url, caption="", reply_to=0):
281
282
  """Отправляет фото по URL или локальному пути с подписью.
282
283
 
283
284
  Args:
284
285
  peer: ID чата, username или "me"
285
286
  photo_path_or_url: URL фото или путь к локальному файлу
286
287
  caption: подпись к фото (опционально)
288
+ reply_to: ID сообщения, на которое отвечаем (опционально)
287
289
  """
288
290
  import os
289
291
  import tempfile
@@ -341,7 +343,7 @@ class WinterGramClient:
341
343
  })
342
344
 
343
345
  # Отправляем медиа-сообщение
344
- result = await self.conn.rpc("messages.sendMedia", {
346
+ media_params = {
345
347
  "peer": peer_obj,
346
348
  "media": {
347
349
  "_": "inputMediaUploadedPhoto",
@@ -355,7 +357,19 @@ class WinterGramClient:
355
357
  },
356
358
  "message": caption,
357
359
  "random_id": random.randint(1, 2**63 - 1)
358
- })
360
+ }
361
+ media_params.update(self.functions.reply_params("messages.sendMedia", reply_to))
362
+
363
+ try:
364
+ result = await self.conn.rpc("messages.sendMedia", media_params)
365
+ except Exception as e:
366
+ if not reply_to:
367
+ raise
368
+ # сервер не принял ответ на сообщение — шлём фото без него
369
+ __log__.warning("sendMedia with reply_to failed (%r), retrying without", e)
370
+ media_params.pop("reply_to", None)
371
+ media_params.pop("reply_to_msg_id", None)
372
+ result = await self.conn.rpc("messages.sendMedia", media_params)
359
373
 
360
374
  return result
361
375
 
@@ -566,6 +580,170 @@ class WinterGramClient:
566
580
  """
567
581
  return await self.functions.update_status(offline)
568
582
 
583
+ # ------------------------------------------------------------------ #
584
+ # профиль: юзернейм, аватар, приватность, сессии
585
+ # ------------------------------------------------------------------ #
586
+
587
+ async def check_username(self, username):
588
+ """Проверяет, свободен ли юзернейм.
589
+
590
+ Args:
591
+ username: имя без @
592
+
593
+ Returns:
594
+ True, если можно занять
595
+ """
596
+ return await self.functions.check_username(username)
597
+
598
+ async def set_username(self, username):
599
+ """Меняет основной юзернейм аккаунта.
600
+
601
+ Args:
602
+ username: новый юзернейм без @
603
+
604
+ Raises:
605
+ RPCError: если юзернейм занят или сервер его отклонил
606
+ """
607
+ return await self.functions.update_username((username or "").lstrip("@"))
608
+
609
+ async def remove_username(self):
610
+ """Снимает основной юзернейм (то же, что set_username(""))."""
611
+ return await self.functions.update_username("")
612
+
613
+ async def toggle_username(self, username, active=True):
614
+ """Включает/выключает дополнительный юзернейм."""
615
+ return await self.functions.toggle_username((username or "").lstrip("@"), active)
616
+
617
+ async def reorder_usernames(self, order):
618
+ """Задаёт порядок юзернеймов (первый становится основным)."""
619
+ return await self.functions.reorder_usernames([u.lstrip("@") for u in order])
620
+
621
+ async def set_avatar(self, photo, fallback=None):
622
+ """Загружает новую аватарку.
623
+
624
+ Args:
625
+ photo: путь к файлу, http(s)-ссылка или сами байты
626
+ fallback: True - поставить как запасную аватарку
627
+
628
+ Returns:
629
+ photos.Photo от сервера
630
+ """
631
+ data = await self._load_media(photo)
632
+ if isinstance(photo, str) and not photo.startswith(("http://", "https://")):
633
+ name = os.path.basename(photo) or "avatar.jpg"
634
+ else:
635
+ name = "avatar.jpg"
636
+ return await self.functions.upload_profile_photo(data, file_name=name, fallback=fallback)
637
+
638
+ async def remove_avatar(self, photo_id=None):
639
+ """Удаляет аватарки.
640
+
641
+ Args:
642
+ photo_id: id фото; если не задан - удаляется самая свежая своя
643
+ """
644
+ if photo_id is None:
645
+ _, res = await self.functions.get_profile_photos(limit=1)
646
+ photos = res.get("photos") or []
647
+ if not photos:
648
+ raise WinterGramError("no profile photos to remove")
649
+ photo_id = photos[0].get("id")
650
+ return await self.functions.delete_profile_photo(photo_id)
651
+
652
+ async def get_avatars(self, peer=None, limit=20):
653
+ """Возвращает список аватарок (своих, если peer не задан)."""
654
+ user = await self._resolve_input_user(peer) if peer is not None else None
655
+ _, result = await self.functions.get_profile_photos(user_id=user, limit=limit)
656
+ return result.get("photos", [])
657
+
658
+ async def get_profile(self, peer=None):
659
+ """Полная информация о профиле (users.getFullUser)."""
660
+ if peer is None:
661
+ return await self.functions.get_full_user(self.functions.input_user_self())
662
+ return await self.functions.get_full_user(await self._resolve_input_user(peer))
663
+
664
+ async def set_privacy(self, key, rules):
665
+ """Меняет правила приватности.
666
+
667
+ Args:
668
+ key: last_seen | phone_number | profile_photo | chat_invite | forwards | about
669
+ rules: ['all'] | ['contacts'] | ['close_friends'] | ['nobody'] |
670
+ ['bots'] | ['premium'] | [('chats', chats)]
671
+
672
+ Examples:
673
+ await client.set_privacy("last_seen", ["nobody"])
674
+ await client.set_privacy("profile_photo", ["contacts"])
675
+ """
676
+ return await self.functions.set_privacy(key, rules)
677
+
678
+ async def get_privacy(self, key):
679
+ """Текущие правила приватности по ключу."""
680
+ return await self.functions.get_privacy(key)
681
+
682
+ async def set_birthday(self, day, month, year=None):
683
+ """Задаёт день рождения в профиле."""
684
+ return await self.functions.update_birthday(day, month, year)
685
+
686
+ async def set_profile_color(self, color=None, for_profile=True):
687
+ """Задаёт цвет профиля (None - сбросить)."""
688
+ return await self.functions.update_profile_color(color, for_profile)
689
+
690
+ async def set_emoji_status(self, emoji=None):
691
+ """Ставит эмодзи-статус (None - очистить)."""
692
+ return await self.functions.set_emoji_status(emoji)
693
+
694
+ async def get_sessions(self):
695
+ """Список активных сессий аккаунта (account.getAuthorizations)."""
696
+ _, result = await self.functions.get_authorizations()
697
+ return result.get("authorizations", [])
698
+
699
+ async def terminate_session(self, auth_hash):
700
+ """Завершает чужую сессию по её hash."""
701
+ return await self.functions.reset_authorization(auth_hash)
702
+
703
+ async def get_account_ttl(self):
704
+ """Текущая настройка автоудаления аккаунта."""
705
+ return await self.functions.get_account_ttl()
706
+
707
+ async def set_account_ttl(self, days=0):
708
+ """Автоудаление аккаунта через N дней (0 — выключить)."""
709
+ return await self.functions.set_account_ttl(days)
710
+
711
+ async def _load_media(self, source):
712
+ """Приводит путь / http-ссылку / bytes к байтам файла."""
713
+ if isinstance(source, (bytes, bytearray)):
714
+ return bytes(source)
715
+ import urllib.request
716
+
717
+ if isinstance(source, str) and source.startswith(("http://", "https://")):
718
+ def _download():
719
+ with urllib.request.urlopen(source, timeout=30) as resp:
720
+ return resp.read()
721
+ return await asyncio.to_thread(_download)
722
+
723
+ if isinstance(source, str) and os.path.isfile(source):
724
+ def _read():
725
+ with open(source, "rb") as f:
726
+ return f.read()
727
+ return await asyncio.to_thread(_read)
728
+
729
+ raise WinterGramError(
730
+ "expected bytes, file path or http(s) URL, got %r" % type(source).__name__
731
+ )
732
+
733
+ async def _resolve_input_user(self, peer):
734
+ """InputPeer (или 'me'/None -> self) -> InputUser для users.*/photos.*."""
735
+ peer_obj = await self._resolve_peer_helper(peer)
736
+ if peer_obj is None or "user_id" not in peer_obj:
737
+ raise WinterGramError("peer %r is not a user" % (peer,))
738
+ _, res = await self.functions.get_users([
739
+ self.functions.input_user(peer_obj["user_id"], peer_obj.get("access_hash"))
740
+ ])
741
+ users = res if isinstance(res, list) else (res.get("users") if isinstance(res, dict) else None)
742
+ if not users:
743
+ raise WinterGramError("cannot resolve user for peer %r" % (peer,))
744
+ user = users[0]
745
+ return self.functions.input_user(user.get("id"), user.get("access_hash"))
746
+
569
747
  async def join_chat(self, peer):
570
748
  """Присоединяется к каналу/супергруппе.
571
749
 
@@ -628,6 +806,106 @@ class WinterGramClient:
628
806
  _, obj = await self.conn.rpc(method, params)
629
807
  return obj
630
808
 
809
+ # ------------------------------------------------------------------ #
810
+ # подарки (star gifts и NFT)
811
+ # ------------------------------------------------------------------ #
812
+
813
+ async def get_available_gifts(self):
814
+ """Получает список всех доступных подарков для отправки.
815
+
816
+ Returns:
817
+ Список подарков с информацией о цене, иконке и т.д.
818
+ """
819
+ _, result = await self.functions.get_star_gifts()
820
+ return result.get("gifts", [])
821
+
822
+ async def send_gift(self, peer, gift_id, message=None, hide_name=False):
823
+ """Отправляет подарок (мишка, NFT и т.д.) пользователю.
824
+
825
+ Args:
826
+ peer: ID пользователя, username или "me"
827
+ gift_id: ID подарка из списка get_available_gifts()
828
+ message: текст сообщения к подарку (опционально)
829
+ hide_name: True - скрыть имя отправителя, False - показать
830
+
831
+ Returns:
832
+ Результат отправки подарка
833
+
834
+ Example:
835
+ # Получаем список подарков
836
+ gifts = await client.get_available_gifts()
837
+ # Отправляем первый подарок с сообщением
838
+ await client.send_gift(user_id, gifts[0]["id"], "С Днём Рождения!")
839
+ """
840
+ peer_obj = await self._resolve_peer_helper(peer)
841
+
842
+ # Проверяем, можно ли отправить этот подарок
843
+ try:
844
+ _, check_result = await self.functions.check_can_send_gift(gift_id)
845
+ result_type = check_result.get("_", "")
846
+ if "Fail" in result_type:
847
+ reason = check_result.get("reason", {})
848
+ reason_text = reason.get("text", "Unknown reason") if isinstance(reason, dict) else str(reason)
849
+ raise RPCError(0, f"Cannot send gift: {reason_text}")
850
+ except RPCError as e:
851
+ if "METHOD_NOT_FOUND" not in str(e):
852
+ raise
853
+ # Метод checkCanSendGift не поддерживается, продолжаем
854
+
855
+ # Отправляем подарок
856
+ return await self.functions.send_star_gift(peer_obj, gift_id, message, hide_name)
857
+
858
+ async def get_my_gifts(self, exclude_unsaved=False, limit=100):
859
+ """Получает подарки, полученные текущим пользователем.
860
+
861
+ Args:
862
+ exclude_unsaved: True - только сохраненные подарки
863
+ limit: максимальное количество подарков
864
+
865
+ Returns:
866
+ Список полученных подарков
867
+ """
868
+ peer_obj = self.functions.input_peer_self()
869
+ _, result = await self.functions.get_saved_star_gifts(
870
+ peer_obj,
871
+ offset="",
872
+ limit=limit,
873
+ exclude_unsaved=exclude_unsaved
874
+ )
875
+ return result.get("gifts", [])
876
+
877
+ async def convert_gift_to_stars(self, msg_id):
878
+ """Конвертирует полученный подарок в звезды (Stars).
879
+
880
+ Args:
881
+ msg_id: ID сообщения с подарком
882
+
883
+ Returns:
884
+ Результат конвертации
885
+
886
+ Note:
887
+ Конвертированный подарок исчезнет из коллекции!
888
+ """
889
+ return await self.functions.convert_star_gift(msg_id)
890
+
891
+ async def transfer_gift(self, msg_id, to_peer):
892
+ """Переводит уникальный подарок (NFT) другому пользователю.
893
+
894
+ Args:
895
+ msg_id: ID сообщения с подарком
896
+ to_peer: ID получателя, username или "me"
897
+
898
+ Returns:
899
+ Результат перевода
900
+
901
+ Note:
902
+ Работает только для уникальных подарков (NFT), которые можно передавать
903
+ """
904
+ to_peer_obj = await self._resolve_peer_helper(to_peer)
905
+ return await self.functions.transfer_star_gift(msg_id, to_peer_obj)
906
+
907
+ # ------------------------------------------------------------------ #
908
+
631
909
  async def __aenter__(self):
632
910
  return await self.connect()
633
911
 
@@ -1,30 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wintergram-userbot
3
- Version: 1.1.2
4
- Summary: Полнофункциональный асинхронный MTProto-клиент для приватных Telegram-серверов (layers 167-228) с поддержкой отправки медиа
5
- Author-email: femboychik <your.email@example.com>
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/femboychik/wintergram-userbot
8
- Project-URL: Repository, https://github.com/femboychik/wintergram-userbot
9
- Project-URL: Issues, https://github.com/femboychik/wintergram-userbot/issues
10
- Keywords: telegram,mtproto,userbot,async,client
11
- Classifier: Development Status :: 4 - Beta
12
- Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: MIT License
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Operating System :: OS Independent
21
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
- Classifier: Topic :: Communications :: Chat
3
+ Version: 1.2
4
+ Summary: WinterGram Userbot for custom MTProto server
5
+ Author: WinterGram Team
23
6
  Requires-Python: >=3.8
24
7
  Description-Content-Type: text/markdown
25
- License-File: LICENSE
26
- Requires-Dist: aiohttp>=3.8.0
27
- Dynamic: license-file
8
+ Requires-Dist: pycryptodome>=3.19.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
28
11
 
29
12
  # wintergram-userbot
30
13
 
@@ -1,4 +1,3 @@
1
- LICENSE
2
1
  MANIFEST.in
3
2
  README.md
4
3
  pyproject.toml
@@ -0,0 +1,4 @@
1
+ pycryptodome>=3.19.0
2
+
3
+ [dev]
4
+ pytest>=7.0.0
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 femboychik
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,17 +0,0 @@
1
- include README.md
2
- include LICENSE
3
- include pyproject.toml
4
- recursive-include wintergram_userbot *.py
5
- recursive-include wintergram_userbot/tl/schemas/data *.json
6
- exclude userbot_demo.py
7
- exclude check_session.py
8
- exclude wintergram_session.db
9
- exclude промпт.txt
10
- recursive-exclude tests *
11
- recursive-exclude tools *
12
- recursive-exclude __pycache__ *
13
- recursive-exclude .pytest_cache *
14
- recursive-exclude .claude *
15
- global-exclude *.pyc
16
- global-exclude *.pyo
17
- global-exclude *~
@@ -1,43 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "wintergram-userbot"
7
- version = "1.1.2"
8
- description = "Полнофункциональный асинхронный MTProto-клиент для приватных Telegram-серверов (layers 167-228) с поддержкой отправки медиа"
9
- readme = "README.md"
10
- requires-python = ">=3.8"
11
- license = {text = "MIT"}
12
- authors = [
13
- {name = "femboychik", email = "your.email@example.com"}
14
- ]
15
- dependencies = [
16
- "aiohttp>=3.8.0"
17
- ]
18
- keywords = ["telegram", "mtproto", "userbot", "async", "client"]
19
- classifiers = [
20
- "Development Status :: 4 - Beta",
21
- "Intended Audience :: Developers",
22
- "License :: OSI Approved :: MIT License",
23
- "Programming Language :: Python :: 3",
24
- "Programming Language :: Python :: 3.8",
25
- "Programming Language :: Python :: 3.9",
26
- "Programming Language :: Python :: 3.10",
27
- "Programming Language :: Python :: 3.11",
28
- "Programming Language :: Python :: 3.12",
29
- "Operating System :: OS Independent",
30
- "Topic :: Software Development :: Libraries :: Python Modules",
31
- "Topic :: Communications :: Chat",
32
- ]
33
-
34
- [project.urls]
35
- Homepage = "https://github.com/femboychik/wintergram-userbot"
36
- Repository = "https://github.com/femboychik/wintergram-userbot"
37
- Issues = "https://github.com/femboychik/wintergram-userbot/issues"
38
-
39
- [tool.setuptools]
40
- packages = ["wintergram_userbot"]
41
-
42
- [tool.setuptools.package-data]
43
- wintergram_userbot = ["tl/schemas/data/*.json"]
@@ -1 +0,0 @@
1
- aiohttp>=3.8.0