vibegram 1.0.0__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 (41) hide show
  1. vibegram-1.0.0/LICENSE +21 -0
  2. vibegram-1.0.0/PKG-INFO +72 -0
  3. vibegram-1.0.0/README.md +53 -0
  4. vibegram-1.0.0/botapi/__init__.py +0 -0
  5. vibegram-1.0.0/pyproject.toml +37 -0
  6. vibegram-1.0.0/scripts/__init__.py +0 -0
  7. vibegram-1.0.0/scripts/backup_chat.py +75 -0
  8. vibegram-1.0.0/scripts/chat_info.py +272 -0
  9. vibegram-1.0.0/scripts/name_clock.py +135 -0
  10. vibegram-1.0.0/scripts/restore_name.py +54 -0
  11. vibegram-1.0.0/scripts/vibe.py +110 -0
  12. vibegram-1.0.0/setup.cfg +4 -0
  13. vibegram-1.0.0/userapi/__init__.py +20 -0
  14. vibegram-1.0.0/userapi/_log.py +14 -0
  15. vibegram-1.0.0/userapi/_tl_auto.py +108 -0
  16. vibegram-1.0.0/userapi/account.py +219 -0
  17. vibegram-1.0.0/userapi/backup.py +267 -0
  18. vibegram-1.0.0/userapi/channels.py +82 -0
  19. vibegram-1.0.0/userapi/chatlists.py +31 -0
  20. vibegram-1.0.0/userapi/client.py +638 -0
  21. vibegram-1.0.0/userapi/communities.py +34 -0
  22. vibegram-1.0.0/userapi/connection/__init__.py +0 -0
  23. vibegram-1.0.0/userapi/connection/telethon_ws.py +168 -0
  24. vibegram-1.0.0/userapi/connection/vibe_keys.py +63 -0
  25. vibegram-1.0.0/userapi/contacts.py +143 -0
  26. vibegram-1.0.0/userapi/crypto/__init__.py +0 -0
  27. vibegram-1.0.0/userapi/crypto/obfuscated.py +43 -0
  28. vibegram-1.0.0/userapi/crypto/vibe_auth.py +88 -0
  29. vibegram-1.0.0/userapi/extra.py +49 -0
  30. vibegram-1.0.0/userapi/folders.py +47 -0
  31. vibegram-1.0.0/userapi/messages.py +253 -0
  32. vibegram-1.0.0/userapi/photos.py +41 -0
  33. vibegram-1.0.0/userapi/tests/__init__.py +0 -0
  34. vibegram-1.0.0/userapi/tests/examples/login_interactive.py +69 -0
  35. vibegram-1.0.0/userapi/users.py +38 -0
  36. vibegram-1.0.0/vibegram.egg-info/PKG-INFO +72 -0
  37. vibegram-1.0.0/vibegram.egg-info/SOURCES.txt +39 -0
  38. vibegram-1.0.0/vibegram.egg-info/dependency_links.txt +1 -0
  39. vibegram-1.0.0/vibegram.egg-info/entry_points.txt +2 -0
  40. vibegram-1.0.0/vibegram.egg-info/requires.txt +9 -0
  41. vibegram-1.0.0/vibegram.egg-info/top_level.txt +3 -0
vibegram-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ali VEX
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.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: vibegram
3
+ Version: 1.0.0
4
+ Summary: Python client library for Vibe Messenger (MTProto over WebSocket)
5
+ Author-email: Ali VEX <lyrdaadhrpyk802@gmail.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: websockets>=13.0
11
+ Requires-Dist: pycryptodome>=3.20
12
+ Requires-Dist: telethon>=1.36
13
+ Requires-Dist: python-socks[asyncio]>=2.4
14
+ Requires-Dist: rsa>=4.9
15
+ Provides-Extra: dev
16
+ Requires-Dist: scapy>=2.5; extra == "dev"
17
+ Requires-Dist: pytest>=7.0; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # pyvibe
21
+
22
+ Python client library for Vibe Messenger (vibegram.chat).
23
+ MTProto over WebSocket with Vibe's custom authentication scheme.
24
+
25
+ ## About the Author
26
+ - Name: Ali VEX
27
+ - Email: lyrdaadhrpyk802@gmail.com
28
+ - GitHub: lyrdaadhrpyk802-ux
29
+ - Jabber: @UserBoT
30
+
31
+ (درباره سازنده: علی، توسعه‌دهنده پایتون. این پروژه در محیط Termux اندروید بدون روت توسعه یافته است.)
32
+
33
+ ## Project Status
34
+ - 852 high-level methods available directly on VibeClient.
35
+ - Full passthrough to 875 Telethon TL requests (raw API).
36
+ - Verified end-to-end: Login, Messaging, Media, Stories, Forums, JSON Backups.
37
+
38
+ (وضعیت پروژه: کتابخانه به صورت کامل روی سرور Vibe تست شده است. احراز هویت اختصاصی Vibe، ارسال و دریافت مدیا، استوری، فروم و بکاپ‌گیری کاملاً عملیاتی هستند.)
39
+
40
+ ## Installation
41
+ cd pyvibe
42
+ pip install -e .
43
+
44
+ ## Quick Start
45
+ import asyncio
46
+ from userapi import VibeClient
47
+
48
+ async def main():
49
+ c = VibeClient()
50
+ await c.connect()
51
+
52
+ if not await c.is_authorized():
53
+ phone = input("Phone: ")
54
+ await c.request_code(phone)
55
+ code = input("OTP: ")
56
+ await c.sign_in(phone, code)
57
+
58
+ me = await c.get_me()
59
+ print(f"Logged in as {me.first_name}")
60
+
61
+ await c.send_message("me", "Hello from pyvibe")
62
+ await c.disconnect()
63
+
64
+ asyncio.run(main())
65
+
66
+ ## Documentation
67
+ - ARCHITECTURE.md - Technical details of transport and auth.
68
+ - VIBE_COMPAT.md - Feature support matrix.
69
+ - examples/ - 11 guided examples with Persian comments.
70
+
71
+ ## License
72
+ MIT License
@@ -0,0 +1,53 @@
1
+ # pyvibe
2
+
3
+ Python client library for Vibe Messenger (vibegram.chat).
4
+ MTProto over WebSocket with Vibe's custom authentication scheme.
5
+
6
+ ## About the Author
7
+ - Name: Ali VEX
8
+ - Email: lyrdaadhrpyk802@gmail.com
9
+ - GitHub: lyrdaadhrpyk802-ux
10
+ - Jabber: @UserBoT
11
+
12
+ (درباره سازنده: علی، توسعه‌دهنده پایتون. این پروژه در محیط Termux اندروید بدون روت توسعه یافته است.)
13
+
14
+ ## Project Status
15
+ - 852 high-level methods available directly on VibeClient.
16
+ - Full passthrough to 875 Telethon TL requests (raw API).
17
+ - Verified end-to-end: Login, Messaging, Media, Stories, Forums, JSON Backups.
18
+
19
+ (وضعیت پروژه: کتابخانه به صورت کامل روی سرور Vibe تست شده است. احراز هویت اختصاصی Vibe، ارسال و دریافت مدیا، استوری، فروم و بکاپ‌گیری کاملاً عملیاتی هستند.)
20
+
21
+ ## Installation
22
+ cd pyvibe
23
+ pip install -e .
24
+
25
+ ## Quick Start
26
+ import asyncio
27
+ from userapi import VibeClient
28
+
29
+ async def main():
30
+ c = VibeClient()
31
+ await c.connect()
32
+
33
+ if not await c.is_authorized():
34
+ phone = input("Phone: ")
35
+ await c.request_code(phone)
36
+ code = input("OTP: ")
37
+ await c.sign_in(phone, code)
38
+
39
+ me = await c.get_me()
40
+ print(f"Logged in as {me.first_name}")
41
+
42
+ await c.send_message("me", "Hello from pyvibe")
43
+ await c.disconnect()
44
+
45
+ asyncio.run(main())
46
+
47
+ ## Documentation
48
+ - ARCHITECTURE.md - Technical details of transport and auth.
49
+ - VIBE_COMPAT.md - Feature support matrix.
50
+ - examples/ - 11 guided examples with Persian comments.
51
+
52
+ ## License
53
+ MIT License
File without changes
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vibegram"
7
+ version = "1.0.0"
8
+ description = "Python client library for Vibe Messenger (MTProto over WebSocket)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [
13
+ { name = "Ali VEX", email = "lyrdaadhrpyk802@gmail.com" }
14
+ ]
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ "websockets>=13.0",
18
+ "pycryptodome>=3.20",
19
+ "telethon>=1.36",
20
+ "python-socks[asyncio]>=2.4",
21
+ "rsa>=4.9",
22
+ ]
23
+
24
+ [project.scripts]
25
+ vibe = "scripts.vibe:main"
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "scapy>=2.5",
30
+ "pytest>=7.0",
31
+ ]
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["userapi*", "botapi*", "scripts*"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["userapi/tests"]
File without changes
@@ -0,0 +1,75 @@
1
+ # ============================================
2
+ # scripts/backup_chat.py - CLI to backup a chat to JSON
3
+ # Usage:
4
+ # python -B scripts/backup_chat.py <peer>
5
+ # python -B scripts/backup_chat.py <peer> --limit 1000
6
+ # python -B scripts/backup_chat.py <peer> --no-media
7
+ # python -B scripts/backup_chat.py <peer> --fresh
8
+ # python -B scripts/backup_chat.py <peer> --output ~/my_backups
9
+ # All code and comments are English. No emojis.
10
+ # ============================================
11
+ import argparse
12
+ import asyncio
13
+ import os
14
+ import sys
15
+
16
+ from userapi import VibeClient
17
+ from userapi.backup import backup_chat
18
+
19
+
20
+ async def main():
21
+ ap = argparse.ArgumentParser(prog="backup_chat")
22
+ ap.add_argument("peer", help="username (@x), id, phone, or title")
23
+ ap.add_argument("--limit", type=int, default=None,
24
+ help="max messages per run (default: all)")
25
+ ap.add_argument("--no-media", action="store_true",
26
+ help="skip downloading media files")
27
+ ap.add_argument("--output", default=None,
28
+ help="output root directory (default: ~/pyvibe/backups)")
29
+ ap.add_argument("--fresh", action="store_true",
30
+ help="ignore existing backup and start over")
31
+ ap.add_argument("--quiet", action="store_true",
32
+ help="do not print per-message progress")
33
+ args = ap.parse_args()
34
+
35
+ if os.getenv("VIBE_DEBUG") != "1":
36
+ import logging
37
+ logging.getLogger("telethon.network.mtprotosender").setLevel(logging.CRITICAL)
38
+ logging.getLogger("telethon.network.connection.connection").setLevel(logging.CRITICAL)
39
+ logging.getLogger("websockets").setLevel(logging.CRITICAL)
40
+
41
+ c = VibeClient()
42
+ try:
43
+ await c.connect()
44
+
45
+ def progress(step, total, msg):
46
+ if args.quiet:
47
+ return
48
+ mt = msg.get("media_type") or "-"
49
+ txt = (msg.get("text") or "")[:40].replace("\n", " ")
50
+ print(f" [{step}{('/' + str(total)) if total else ''}] "
51
+ f"id={msg.get('id')} {mt:8s} {txt!r}")
52
+
53
+ path, doc = await backup_chat(
54
+ c, args.peer,
55
+ output_root=args.output,
56
+ limit=args.limit,
57
+ download_media=not args.no_media,
58
+ progress=progress,
59
+ fresh=args.fresh,
60
+ )
61
+ print(f"\n[+] saved {doc['message_count']} message(s), "
62
+ f"{doc['media_count']} media file(s)")
63
+ print(f"[+] json: {path}")
64
+ print(f"[+] dir: {path.parent}")
65
+ except Exception as e:
66
+ print(f"[backup_chat] error: {type(e).__name__}: {e}")
67
+ finally:
68
+ await c.disconnect()
69
+
70
+
71
+ if __name__ == "__main__":
72
+ try:
73
+ asyncio.run(main())
74
+ except KeyboardInterrupt:
75
+ sys.exit(0)
@@ -0,0 +1,272 @@
1
+ # ============================================
2
+ # scripts/chat_info.py - "info" helper
3
+ # Listens for "info" messages you send in any chat and replies with a
4
+ # full dump: users / groups / channels / supergroups.
5
+ # All code and comments are English. No emojis.
6
+ # ============================================
7
+ import asyncio
8
+ import os
9
+ import sys
10
+ from datetime import datetime, timezone
11
+
12
+ from telethon import events
13
+ from telethon.tl.functions.messages import GetFullChatRequest
14
+ from telethon.tl.types import (
15
+ User, UserEmpty, Channel, ChannelForbidden,
16
+ Chat, ChatForbidden,
17
+ )
18
+
19
+ from userapi import VibeClient
20
+
21
+ # Silence benign Telethon shutdown noise (WebSocket ConnectionClosedOK) unless
22
+ # explicitly debugging. This is not part of INFO_DEBUG; keep the terminal clean.
23
+ if os.getenv("VIBE_DEBUG") != "1":
24
+ import logging
25
+ logging.getLogger("telethon.network.mtprotosender").setLevel(logging.CRITICAL)
26
+ logging.getLogger("telethon.network.connection.connection").setLevel(logging.CRITICAL)
27
+ logging.getLogger("websockets").setLevel(logging.CRITICAL)
28
+
29
+
30
+ def _fmt_date(dt):
31
+ if dt is None:
32
+ return "?"
33
+ if isinstance(dt, datetime):
34
+ return dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
35
+ return str(dt)
36
+
37
+
38
+ def _status_str(user):
39
+ s = getattr(user, "status", None)
40
+ if s is None:
41
+ return "?"
42
+ name = type(s).__name__
43
+ if name == "UserStatusOnline":
44
+ return f"online until {_fmt_date(getattr(s, 'expires', None))}"
45
+ if name == "UserStatusOffline":
46
+ return f"offline (last seen {_fmt_date(getattr(s, 'was_online', None))})"
47
+ if name == "UserStatusRecently":
48
+ return "recently"
49
+ if name == "UserStatusLastWeek":
50
+ return "last week"
51
+ if name == "UserStatusLastMonth":
52
+ return "last month"
53
+ return name
54
+
55
+
56
+ async def info_user(client, user):
57
+ lines = ["=== USER INFO ===", f"id: {user.id}"]
58
+ lines.append(f"username: @{user.username}" if getattr(user, "username", None) else "username: -")
59
+ lines.append(f"first_name: {getattr(user, 'first_name', None) or '-'}")
60
+ lines.append(f"last_name: {getattr(user, 'last_name', None) or '-'}")
61
+ lines.append(f"phone: {getattr(user, 'phone', None) or '-'}")
62
+ lines.append(f"bot: {bool(getattr(user, 'bot', False))}")
63
+ lines.append(f"verified: {bool(getattr(user, 'verified', False))}")
64
+ lines.append(f"premium: {bool(getattr(user, 'premium', False))}")
65
+ lines.append(f"deleted: {bool(getattr(user, 'deleted', False))}")
66
+ lines.append(f"status: {_status_str(user)}")
67
+
68
+ if not isinstance(user, UserEmpty):
69
+ try:
70
+ full = await client.get_full_user(user)
71
+ fu = getattr(full, "full_user", None)
72
+ if fu is not None:
73
+ lines.append(f"about: {getattr(fu, 'about', None) or '-'}")
74
+ lines.append(f"common_chats: {getattr(fu, 'common_chats_count', 0)}")
75
+ lines.append(f"blocked: {bool(getattr(fu, 'blocked', False))}")
76
+ lines.append(f"phone_calls_available: {getattr(fu, 'phone_calls_available', '?')}")
77
+ lines.append(f"voice_messages_forbidden: {bool(getattr(fu, 'voice_messages_forbidden', False))}")
78
+ except Exception as e:
79
+ lines.append(f"(full_user fetch failed: {type(e).__name__}: {e})")
80
+ return "\n".join(lines)
81
+
82
+
83
+ async def info_group(client, chat):
84
+ lines = ["=== GROUP INFO ===", f"id: {chat.id}"]
85
+ lines.append(f"title: {getattr(chat, 'title', '-')}")
86
+ lines.append(f"participants_count: {getattr(chat, 'participants_count', '?')}")
87
+ lines.append(f"date: {_fmt_date(getattr(chat, 'date', None))}")
88
+ lines.append(f"creator: {bool(getattr(chat, 'creator', False))}")
89
+ lines.append(f"kicked: {bool(getattr(chat, 'kicked', False))}")
90
+ lines.append(f"deactivated: {bool(getattr(chat, 'deactivated', False))}")
91
+ lines.append(f"version: {getattr(chat, 'version', '?')}")
92
+
93
+ try:
94
+ r = await client.raw(GetFullChatRequest(chat_id=chat.id))
95
+ fc = getattr(r, "full_chat", None)
96
+ if fc is not None:
97
+ lines.append(f"about: {getattr(fc, 'about', None) or '-'}")
98
+ parts = getattr(fc, "participants", None)
99
+ if parts is not None:
100
+ lines.append(f"participants_loaded: {len(getattr(parts, 'participants', []) or [])}")
101
+ lines.append(f"can_set_username: {bool(getattr(fc, 'can_set_username', False))}")
102
+ lines.append(f"can_set_stickers: {bool(getattr(fc, 'can_set_stickers', False))}")
103
+ lines.append(f"can_set_location: {bool(getattr(fc, 'can_set_location', False))}")
104
+ lines.append(f"hidden_prehistory: {bool(getattr(fc, 'hidden_prehistory', False))}")
105
+ except Exception as e:
106
+ lines.append(f"(full_chat fetch failed: {type(e).__name__}: {e})")
107
+ return "\n".join(lines)
108
+
109
+
110
+ async def info_channel(client, channel):
111
+ is_megagroup = bool(getattr(channel, "megagroup", False))
112
+ is_broadcast = bool(getattr(channel, "broadcast", False))
113
+ if is_megagroup:
114
+ kind = "SUPERGROUP"
115
+ elif is_broadcast:
116
+ kind = "CHANNEL"
117
+ else:
118
+ kind = "CHANNEL/GROUP"
119
+
120
+ lines = [f"=== {kind} INFO ===", f"id: {channel.id}"]
121
+ lines.append(f"title: {getattr(channel, 'title', '-')}")
122
+ if getattr(channel, "username", None):
123
+ lines.append(f"username: @{channel.username}")
124
+ else:
125
+ lines.append("username: -")
126
+ lines.append(f"broadcast: {is_broadcast}")
127
+ lines.append(f"megagroup: {is_megagroup}")
128
+ lines.append(f"verified: {bool(getattr(channel, 'verified', False))}")
129
+ lines.append(f"scam: {bool(getattr(channel, 'scam', False))}")
130
+ lines.append(f"fake: {bool(getattr(channel, 'fake', False))}")
131
+ lines.append(f"restricted: {bool(getattr(channel, 'restricted', False))}")
132
+ lines.append(f"signatures: {bool(getattr(channel, 'signatures', False))}")
133
+ lines.append(f"date: {_fmt_date(getattr(channel, 'date', None))}")
134
+ lines.append(f"creator: {bool(getattr(channel, 'creator', False))}")
135
+ lines.append(f"left: {bool(getattr(channel, 'left', False))}")
136
+ lines.append(f"kicked: {bool(getattr(channel, 'kicked', False))}")
137
+ if getattr(channel, "linked_chat_id", None):
138
+ lines.append(f"linked_chat_id: {channel.linked_chat_id}")
139
+ if getattr(channel, "slowmode_seconds", None):
140
+ lines.append(f"slowmode_seconds: {channel.slowmode_seconds}")
141
+
142
+ if isinstance(channel, ChannelForbidden):
143
+ lines.append("(ChannelForbidden: no full info available)")
144
+ return "\n".join(lines)
145
+
146
+ # creator/admin flag check
147
+ try:
148
+ admin_rights = getattr(channel, "admin_rights", None)
149
+ if admin_rights is not None:
150
+ lines.append(f"admin_rights: {admin_rights}")
151
+ except Exception:
152
+ pass
153
+
154
+ try:
155
+ full = await client.get_full_channel(channel)
156
+ fc = getattr(full, "full_chat", None)
157
+ if fc is not None:
158
+ for key in [
159
+ "about", "participants_count", "online_count",
160
+ "admins_count", "kicked_count", "banned_count",
161
+ "can_view_participants", "can_set_username",
162
+ "can_set_stickers", "hidden_prehistory",
163
+ "can_set_location", "has_scheduled",
164
+ ]:
165
+ v = getattr(fc, key, None)
166
+ if isinstance(v, bool):
167
+ lines.append(f"{key}: {v}")
168
+ elif v not in (None, ""):
169
+ lines.append(f"{key}: {v}")
170
+ except Exception as e:
171
+ lines.append(f"(full_channel fetch failed: {type(e).__name__}: {e})")
172
+ return "\n".join(lines)
173
+
174
+
175
+ async def collect_info(client, entity):
176
+ if isinstance(entity, (User, UserEmpty)):
177
+ return await info_user(client, entity)
178
+ if isinstance(entity, (Channel, ChannelForbidden)):
179
+ return await info_channel(client, entity)
180
+ if isinstance(entity, (Chat, ChatForbidden)):
181
+ return await info_group(client, entity)
182
+ return f"=== UNKNOWN entity: {type(entity).__name__} ==="
183
+
184
+
185
+ async def main():
186
+ c = VibeClient()
187
+ try:
188
+ await c.connect()
189
+ me = await c.get_me()
190
+ print(f"[+] connected as id={me.id} @{me.username}")
191
+ print("[+] listening for 'info' / '/info' messages (yours only)")
192
+ print(" Ctrl+C to stop\n")
193
+
194
+ debug = os.getenv("INFO_DEBUG") == "1"
195
+
196
+ @c.raw.on(events.NewMessage())
197
+ async def handler(event):
198
+ text = (event.raw_text or "").strip().lower()
199
+ if text not in ("info", "/info", ".info"):
200
+ return
201
+
202
+ # Robust chat resolution (channels sometimes need fallback)
203
+ chat = await event.get_chat()
204
+ if chat is None and getattr(event, "chat_id", None) is not None:
205
+ try:
206
+ chat = await c.get_entity(event.chat_id)
207
+ except Exception:
208
+ chat = None
209
+ if chat is None:
210
+ if debug:
211
+ print(f"[debug] 'info' msg with no chat: chat_id={getattr(event, 'chat_id', None)}")
212
+ return
213
+
214
+ # Robust sender detection: works for users, groups, supergroups,
215
+ # and channels (both as-user and anonymous-as-channel posts).
216
+ sender = None
217
+ try:
218
+ sender = await event.get_sender()
219
+ except Exception:
220
+ sender = None
221
+
222
+ out = getattr(event, "out", None)
223
+ sender_id = getattr(sender, "id", None) if sender is not None else None
224
+ chat_id = getattr(chat, "id", None)
225
+
226
+ is_mine = (
227
+ out is True
228
+ or sender_id == me.id
229
+ or (chat_id is not None and sender_id == chat_id)
230
+ )
231
+
232
+ if debug:
233
+ print(f"[debug] 'info' in chat_id={chat_id} "
234
+ f"sender_id={sender_id} out={out} is_mine={is_mine}")
235
+
236
+ if not is_mine:
237
+ return
238
+
239
+ try:
240
+ info = await collect_info(c, chat)
241
+ except Exception as e:
242
+ info = f"[chat_info] error: {type(e).__name__}: {e}"
243
+
244
+ title = getattr(chat, "title", None) or getattr(chat, "username", None) or getattr(chat, "id", "?")
245
+ if debug:
246
+ print("\n" + info + "\n")
247
+
248
+ try:
249
+ await event.reply(info)
250
+ print(f"[+] info sent -> {title}")
251
+ except Exception as e:
252
+ print(f"[chat_info] reply failed: {type(e).__name__}: {e}")
253
+ try:
254
+ await c.send_message(chat, info)
255
+ print(f"[+] info sent (fallback) -> {title}")
256
+ except Exception as e2:
257
+ print(f"[chat_info] send_message also failed: {type(e2).__name__}: {e2}")
258
+
259
+ await c.raw.run_until_disconnected()
260
+ except KeyboardInterrupt:
261
+ pass
262
+ except Exception as e:
263
+ print(f"[chat_info] fatal: {type(e).__name__}: {e}")
264
+ finally:
265
+ await c.disconnect()
266
+
267
+
268
+ if __name__ == "__main__":
269
+ try:
270
+ asyncio.run(main())
271
+ except KeyboardInterrupt:
272
+ sys.exit(0)
@@ -0,0 +1,135 @@
1
+ # ============================================
2
+ # scripts/name_clock.py - live clock in first_name
3
+ # Updates first_name to "<original>•HH:MM" (Tehran time) every 70s.
4
+ # Restores original name on Ctrl+C or SIGTERM.
5
+ # All code and comments are English. No emojis.
6
+ # ============================================
7
+ import asyncio
8
+ import os
9
+ import signal
10
+ import sys
11
+ from datetime import datetime, timedelta, timezone
12
+
13
+ from telethon.errors import FloodWaitError
14
+
15
+ from userapi import VibeClient
16
+
17
+
18
+ # Iran: fixed offset UTC+3:30 (no DST since 2022). Using a fixed offset
19
+ # avoids relying on the tzdata package being installed on Termux.
20
+ TEHRAN = timezone(timedelta(hours=3, minutes=30))
21
+
22
+ UPDATE_INTERVAL = 70.0 # seconds between updates
23
+ SEPARATOR = "\u2022" # bullet
24
+ FIRST_NAME_MAX = 64 # Telegram limit (bytes for safety we use chars)
25
+
26
+
27
+ def _tehran_hhmm():
28
+ return datetime.now(TEHRAN).strftime("%H:%M")
29
+
30
+
31
+ def _build_first_name(base, hhmm):
32
+ """Compose base + separator + HH:MM, truncating base if needed."""
33
+ suffix = f"{SEPARATOR}{hhmm}"
34
+ room = FIRST_NAME_MAX - len(suffix)
35
+ if room < 1:
36
+ return suffix[-FIRST_NAME_MAX:]
37
+ return base[:room] + suffix
38
+
39
+
40
+ async def main():
41
+ if os.getenv("VIBE_DEBUG") != "1":
42
+ import logging
43
+ logging.getLogger("telethon.network.mtprotosender").setLevel(logging.CRITICAL)
44
+ logging.getLogger("telethon.network.connection.connection").setLevel(logging.CRITICAL)
45
+ logging.getLogger("websockets").setLevel(logging.CRITICAL)
46
+
47
+ c = VibeClient()
48
+ stop_event = asyncio.Event()
49
+
50
+ def _stop(*_):
51
+ stop_event.set()
52
+
53
+ try:
54
+ loop = asyncio.get_running_loop()
55
+ for sig in (signal.SIGINT, signal.SIGTERM):
56
+ try:
57
+ loop.add_signal_handler(sig, _stop)
58
+ except NotImplementedError:
59
+ # Termux/android fallback
60
+ signal.signal(sig, lambda *_: stop_event.set())
61
+
62
+ await c.connect()
63
+
64
+ me = await c.get_me()
65
+ original_first = getattr(me, "first_name", "") or ""
66
+ original_last = getattr(me, "last_name", "") or ""
67
+ base = original_first.strip()
68
+
69
+ print(f"[+] connected as id={me.id} @{getattr(me, 'username', '-')}")
70
+ print(f"[+] original first_name = {original_first!r}")
71
+ print(f"[+] original last_name = {original_last!r}")
72
+ print(f"[+] base = {base!r}")
73
+ print(f"[+] updating every {int(UPDATE_INTERVAL)}s to '{base}{SEPARATOR}HH:MM' (Tehran)")
74
+ print("[+] Ctrl+C to stop and restore\n")
75
+
76
+ last_sent = None
77
+
78
+ while not stop_event.is_set():
79
+ hhmm = _tehran_hhmm()
80
+ new_first = _build_first_name(base, hhmm)
81
+
82
+ if new_first == last_sent:
83
+ # Avoid redundant update_profile calls when minute didn't
84
+ # change (can happen due to 70s interval).
85
+ if os.getenv("NAME_DEBUG") == "1":
86
+ print(f"[debug] skipping unchanged name {new_first!r}")
87
+ else:
88
+ try:
89
+ await c.update_profile(
90
+ first_name=new_first,
91
+ last_name=original_last or None,
92
+ )
93
+ last_sent = new_first
94
+ print(f"[+] name -> {new_first!r}")
95
+ except FloodWaitError as e:
96
+ wait_s = getattr(e, "seconds", 60) + 5
97
+ print(f"[!] FLOOD_WAIT {wait_s}s, backing off")
98
+ try:
99
+ await asyncio.wait_for(stop_event.wait(), timeout=wait_s)
100
+ except asyncio.TimeoutError:
101
+ pass
102
+ continue
103
+ except Exception as e:
104
+ print(f"[!] update_profile failed: {type(e).__name__}: {e}")
105
+
106
+ # Wait the remaining interval, but wake up early on Ctrl+C
107
+ try:
108
+ await asyncio.wait_for(stop_event.wait(), timeout=UPDATE_INTERVAL)
109
+ except asyncio.TimeoutError:
110
+ pass
111
+
112
+ print("\n[i] stopping, restoring original name...")
113
+ try:
114
+ await c.update_profile(
115
+ first_name=original_first or None,
116
+ last_name=original_last or None,
117
+ )
118
+ print(f"[+] restored first_name={original_first!r} last_name={original_last!r}")
119
+ except Exception as e:
120
+ print(f"[!] restore failed: {type(e).__name__}: {e}")
121
+ print(f" run: python -B scripts/restore_name.py")
122
+ except Exception as e:
123
+ print(f"[name_clock] fatal: {type(e).__name__}: {e}")
124
+ finally:
125
+ try:
126
+ await c.disconnect()
127
+ except Exception:
128
+ pass
129
+
130
+
131
+ if __name__ == "__main__":
132
+ try:
133
+ asyncio.run(main())
134
+ except KeyboardInterrupt:
135
+ pass