Telegram-Py-Arang 0.0.1__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.
- pyrogram/__init__.py +34 -0
- pyrogram/__main__.py +18 -0
- pyrogram/client.py +353 -0
- pyrogram/dispatcher.py +63 -0
- pyrogram/enums/__init__.py +18 -0
- pyrogram/enums/chat_action.py +15 -0
- pyrogram/enums/chat_member_status.py +10 -0
- pyrogram/enums/chat_type.py +8 -0
- pyrogram/enums/message_entity_type.py +23 -0
- pyrogram/enums/message_service_type.py +35 -0
- pyrogram/enums/parse_mode.py +7 -0
- pyrogram/enums/update_type.py +30 -0
- pyrogram/errors/__init__.py +25 -0
- pyrogram/errors/bad_request.py +5 -0
- pyrogram/errors/conflict.py +5 -0
- pyrogram/errors/flood_wait.py +12 -0
- pyrogram/errors/forbidden.py +5 -0
- pyrogram/errors/internal_server_error.py +5 -0
- pyrogram/errors/map_error.py +45 -0
- pyrogram/errors/message_not_modified.py +5 -0
- pyrogram/errors/not_found.py +5 -0
- pyrogram/errors/rpc_error.py +13 -0
- pyrogram/errors/service_unavailable.py +5 -0
- pyrogram/errors/unauthorized.py +5 -0
- pyrogram/filters.py +191 -0
- pyrogram/handlers/__init__.py +23 -0
- pyrogram/handlers/base.py +13 -0
- pyrogram/handlers/callback_query_handler.py +19 -0
- pyrogram/handlers/chat_join_request_handler.py +19 -0
- pyrogram/handlers/chat_member_updated_handler.py +21 -0
- pyrogram/handlers/inline_query_handler.py +19 -0
- pyrogram/handlers/message_handler.py +36 -0
- pyrogram/handlers/message_reaction_handler.py +24 -0
- pyrogram/handlers/poll_answer_handler.py +19 -0
- pyrogram/handlers/poll_handler.py +17 -0
- pyrogram/handlers/raw_update_handler.py +11 -0
- pyrogram/http.py +51 -0
- pyrogram/idle.py +28 -0
- pyrogram/methods.py +425 -0
- pyrogram/py.typed +0 -0
- pyrogram/session.py +132 -0
- pyrogram/sync.py +36 -0
- pyrogram/types/__init__.py +92 -0
- pyrogram/types/business_connection.py +18 -0
- pyrogram/types/business_messages_deleted.py +12 -0
- pyrogram/types/callback_query.py +36 -0
- pyrogram/types/chat.py +128 -0
- pyrogram/types/chat_boost.py +53 -0
- pyrogram/types/chat_join_request.py +34 -0
- pyrogram/types/chat_member.py +61 -0
- pyrogram/types/chat_member_updated.py +39 -0
- pyrogram/types/chosen_inline_result.py +15 -0
- pyrogram/types/file.py +12 -0
- pyrogram/types/inline_query.py +20 -0
- pyrogram/types/keyboard.py +138 -0
- pyrogram/types/labeled_price.py +15 -0
- pyrogram/types/link_preview_options.py +15 -0
- pyrogram/types/message.py +300 -0
- pyrogram/types/message_entity.py +17 -0
- pyrogram/types/message_origin.py +71 -0
- pyrogram/types/message_reaction_count_updated.py +14 -0
- pyrogram/types/message_reaction_updated.py +27 -0
- pyrogram/types/object.py +22 -0
- pyrogram/types/order_info.py +16 -0
- pyrogram/types/paid_media.py +44 -0
- pyrogram/types/poll.py +47 -0
- pyrogram/types/poll_answer.py +16 -0
- pyrogram/types/pre_checkout_query.py +27 -0
- pyrogram/types/purchased_paid_media.py +13 -0
- pyrogram/types/reaction_type.py +27 -0
- pyrogram/types/refunded_payment.py +16 -0
- pyrogram/types/reply_parameters.py +29 -0
- pyrogram/types/shipping_address.py +15 -0
- pyrogram/types/shipping_option.py +14 -0
- pyrogram/types/shipping_query.py +17 -0
- pyrogram/types/successful_payment.py +26 -0
- pyrogram/types/user.py +35 -0
- pyrogram/types/vanue.py +19 -0
- pyrogram/types/web_app_data.py +10 -0
- pyrogram/utils/__init__.py +18 -0
- pyrogram/utils/file.py +35 -0
- pyrogram/utils/helpers.py +36 -0
- telegram_py_arang-0.0.1.dist-info/METADATA +481 -0
- telegram_py_arang-0.0.1.dist-info/RECORD +87 -0
- telegram_py_arang-0.0.1.dist-info/WHEEL +5 -0
- telegram_py_arang-0.0.1.dist-info/licenses/LICENSE +674 -0
- telegram_py_arang-0.0.1.dist-info/top_level.txt +1 -0
pyrogram/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TelePy
|
|
3
|
+
~~~~~~
|
|
4
|
+
Pyrogram-style framework for Telegram bots.
|
|
5
|
+
Bot-only. Backend: Telegram Bot API 8.x via aiohttp.
|
|
6
|
+
Tidak ada userbot, tidak ada MTProto, tidak ada crypto.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.0.1"
|
|
10
|
+
__license__ = "LGPL-3.0-or-later"
|
|
11
|
+
__author__ = "TelePy Contributors"
|
|
12
|
+
|
|
13
|
+
from .client import Client
|
|
14
|
+
from .idle import idle
|
|
15
|
+
from .session import Session, MemoryStorage, SQLiteStorage
|
|
16
|
+
from .types import (
|
|
17
|
+
User, Chat, ChatMember, Message, CallbackQuery, InlineQuery,
|
|
18
|
+
InlineKeyboardMarkup, InlineKeyboardButton,
|
|
19
|
+
ReplyKeyboardMarkup, KeyboardButton,
|
|
20
|
+
ReplyKeyboardRemove, ForceReply, CopyTextButton,
|
|
21
|
+
LinkPreviewOptions, ReplyParameters,
|
|
22
|
+
)
|
|
23
|
+
from . import filters, enums, errors, handlers, types, utils
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Client", "idle", "Session", "MemoryStorage", "SQLiteStorage",
|
|
27
|
+
"filters", "enums", "errors", "handlers", "types", "utils",
|
|
28
|
+
"User", "Chat", "ChatMember", "Message",
|
|
29
|
+
"CallbackQuery", "InlineQuery",
|
|
30
|
+
"InlineKeyboardMarkup", "InlineKeyboardButton",
|
|
31
|
+
"ReplyKeyboardMarkup", "KeyboardButton",
|
|
32
|
+
"ReplyKeyboardRemove", "ForceReply", "CopyTextButton",
|
|
33
|
+
"LinkPreviewOptions", "ReplyParameters",
|
|
34
|
+
]
|
pyrogram/__main__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Entry point CLI: `telepy <script.py>` atau `python -m pyrogram <script.py>`."""
|
|
2
|
+
|
|
3
|
+
import runpy
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
if len(sys.argv) < 2:
|
|
9
|
+
print("TelePy — Pyrogram bot-only fork")
|
|
10
|
+
print("Usage: telepy <module.py>")
|
|
11
|
+
print(" or: python -m pyrogram <module.py>")
|
|
12
|
+
sys.exit(0)
|
|
13
|
+
|
|
14
|
+
runpy.run_path(sys.argv[1], run_name="__main__")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
main()
|
pyrogram/client.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client — pintu masuk utama TelePy.
|
|
3
|
+
|
|
4
|
+
Contoh:
|
|
5
|
+
from pyrogram import Client, filters
|
|
6
|
+
|
|
7
|
+
app = Client("BOT_TOKEN")
|
|
8
|
+
|
|
9
|
+
@app.on_message(filters.text)
|
|
10
|
+
async def echo(client, message):
|
|
11
|
+
await message.reply_text(message.text)
|
|
12
|
+
|
|
13
|
+
app.run()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from .dispatcher import Dispatcher
|
|
21
|
+
from .errors import map_error
|
|
22
|
+
from .filters import filters
|
|
23
|
+
from .handlers import (
|
|
24
|
+
MessageHandler,
|
|
25
|
+
CallbackQueryHandler,
|
|
26
|
+
InlineQueryHandler,
|
|
27
|
+
ChatMemberUpdatedHandler,
|
|
28
|
+
ChatJoinRequestHandler,
|
|
29
|
+
PollHandler,
|
|
30
|
+
PollAnswerHandler,
|
|
31
|
+
MessageReactionHandler,
|
|
32
|
+
RawUpdateHandler,
|
|
33
|
+
)
|
|
34
|
+
from .http import get_session, close_pool
|
|
35
|
+
from .methods import AllMethods
|
|
36
|
+
from .session import Session, MemoryStorage
|
|
37
|
+
|
|
38
|
+
log = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
DEFAULT_API = "https://api.telegram.org/bot{token}/{method}"
|
|
41
|
+
DEFAULT_FILE = "https://api.telegram.org/file/bot{token}/{path}"
|
|
42
|
+
|
|
43
|
+
# Update yang diminta dari getUpdates
|
|
44
|
+
ALLOWED_UPDATES = [
|
|
45
|
+
"message",
|
|
46
|
+
"edited_message",
|
|
47
|
+
"channel_post",
|
|
48
|
+
"edited_channel_post",
|
|
49
|
+
"callback_query",
|
|
50
|
+
"inline_query",
|
|
51
|
+
"chosen_inline_result",
|
|
52
|
+
"my_chat_member",
|
|
53
|
+
"chat_member",
|
|
54
|
+
"chat_join_request",
|
|
55
|
+
"poll",
|
|
56
|
+
"poll_answer",
|
|
57
|
+
"message_reaction",
|
|
58
|
+
"message_reaction_count",
|
|
59
|
+
"pre_checkout_query",
|
|
60
|
+
"shipping_query",
|
|
61
|
+
"business_connection",
|
|
62
|
+
"business_message",
|
|
63
|
+
"edited_business_message",
|
|
64
|
+
"purchased_paid_media",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Client(AllMethods):
|
|
69
|
+
"""
|
|
70
|
+
Pyrogram-style bot client. Bot-only. Multi-bot friendly.
|
|
71
|
+
|
|
72
|
+
Parameter:
|
|
73
|
+
bot_token : Token bot dari @BotFather (wajib).
|
|
74
|
+
name : Nama session file (default "pyrogram").
|
|
75
|
+
parse_mode : "HTML" / "Markdown" / "MarkdownV2" / None.
|
|
76
|
+
workers : Batas konkurensi request.
|
|
77
|
+
storage : Objek storage (MemoryStorage default).
|
|
78
|
+
proxy : URL proxy aiohttp (opsional).
|
|
79
|
+
flood_retry : Retry otomatis saat FloodWait (default True).
|
|
80
|
+
store_raw : Simpan raw JSON di tipe (default False, hemat RAM).
|
|
81
|
+
api_url : URL Bot API kustom (mis. local Bot API server).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
bot_token: str = None,
|
|
87
|
+
name: str = "pyrogram",
|
|
88
|
+
parse_mode: Optional[str] = None,
|
|
89
|
+
workers: int = 4,
|
|
90
|
+
storage=None,
|
|
91
|
+
proxy: Optional[str] = None,
|
|
92
|
+
flood_retry: bool = True,
|
|
93
|
+
store_raw: bool = False,
|
|
94
|
+
api_url: Optional[str] = None,
|
|
95
|
+
):
|
|
96
|
+
if not bot_token:
|
|
97
|
+
raise ValueError("bot_token wajib diisi")
|
|
98
|
+
|
|
99
|
+
self.bot_token = bot_token
|
|
100
|
+
self.name = name
|
|
101
|
+
self.parse_mode = parse_mode
|
|
102
|
+
self.workers = workers
|
|
103
|
+
self.proxy = proxy
|
|
104
|
+
self.flood_retry = flood_retry
|
|
105
|
+
self.store_raw = store_raw
|
|
106
|
+
|
|
107
|
+
self.api_url = api_url or DEFAULT_API
|
|
108
|
+
self.file_url = DEFAULT_FILE
|
|
109
|
+
|
|
110
|
+
self.session = Session(name)
|
|
111
|
+
self.storage = storage or MemoryStorage()
|
|
112
|
+
self.dispatcher = Dispatcher(self)
|
|
113
|
+
|
|
114
|
+
self._offset = self.session.get("offset", 0)
|
|
115
|
+
self._running = False
|
|
116
|
+
self._poll_task = None
|
|
117
|
+
self._sem = asyncio.Semaphore(workers)
|
|
118
|
+
self._http = None
|
|
119
|
+
self.me = None
|
|
120
|
+
|
|
121
|
+
# =================== Decorators ===================
|
|
122
|
+
def on_message(self, filter=None, group: int = 0):
|
|
123
|
+
f = filter or filters.all
|
|
124
|
+
|
|
125
|
+
def deco(func):
|
|
126
|
+
self.dispatcher.add_handler(
|
|
127
|
+
MessageHandler(func, f, group, edited=False)
|
|
128
|
+
)
|
|
129
|
+
return func
|
|
130
|
+
return deco
|
|
131
|
+
|
|
132
|
+
def on_edited_message(self, filter=None, group: int = 0):
|
|
133
|
+
f = filter or filters.all
|
|
134
|
+
|
|
135
|
+
def deco(func):
|
|
136
|
+
self.dispatcher.add_handler(
|
|
137
|
+
MessageHandler(func, f, group, edited=True)
|
|
138
|
+
)
|
|
139
|
+
return func
|
|
140
|
+
return deco
|
|
141
|
+
|
|
142
|
+
def on_channel_post(self, filter=None, group: int = 0):
|
|
143
|
+
f = filter or filters.all
|
|
144
|
+
|
|
145
|
+
def deco(func):
|
|
146
|
+
self.dispatcher.add_handler(
|
|
147
|
+
MessageHandler(func, f, group, edited=False)
|
|
148
|
+
)
|
|
149
|
+
return func
|
|
150
|
+
return deco
|
|
151
|
+
|
|
152
|
+
def on_edited_channel_post(self, filter=None, group: int = 0):
|
|
153
|
+
f = filter or filters.all
|
|
154
|
+
|
|
155
|
+
def deco(func):
|
|
156
|
+
self.dispatcher.add_handler(
|
|
157
|
+
MessageHandler(func, f, group, edited=True)
|
|
158
|
+
)
|
|
159
|
+
return func
|
|
160
|
+
return deco
|
|
161
|
+
|
|
162
|
+
def on_callback_query(self, filter=None, group: int = 0):
|
|
163
|
+
f = filter or filters.all
|
|
164
|
+
|
|
165
|
+
def deco(func):
|
|
166
|
+
self.dispatcher.add_handler(CallbackQueryHandler(func, f, group))
|
|
167
|
+
return func
|
|
168
|
+
return deco
|
|
169
|
+
|
|
170
|
+
def on_inline_query(self, filter=None, group: int = 0):
|
|
171
|
+
f = filter or filters.all
|
|
172
|
+
|
|
173
|
+
def deco(func):
|
|
174
|
+
self.dispatcher.add_handler(InlineQueryHandler(func, f, group))
|
|
175
|
+
return func
|
|
176
|
+
return deco
|
|
177
|
+
|
|
178
|
+
def on_chat_member_updated(self, filter=None, group: int = 0):
|
|
179
|
+
f = filter or filters.all
|
|
180
|
+
|
|
181
|
+
def deco(func):
|
|
182
|
+
self.dispatcher.add_handler(ChatMemberUpdatedHandler(func, f, group))
|
|
183
|
+
return func
|
|
184
|
+
return deco
|
|
185
|
+
|
|
186
|
+
def on_chat_join_request(self, filter=None, group: int = 0):
|
|
187
|
+
f = filter or filters.all
|
|
188
|
+
|
|
189
|
+
def deco(func):
|
|
190
|
+
self.dispatcher.add_handler(ChatJoinRequestHandler(func, f, group))
|
|
191
|
+
return func
|
|
192
|
+
return deco
|
|
193
|
+
|
|
194
|
+
def on_poll(self, filter=None, group: int = 0):
|
|
195
|
+
f = filter or filters.all
|
|
196
|
+
|
|
197
|
+
def deco(func):
|
|
198
|
+
self.dispatcher.add_handler(PollHandler(func, f, group))
|
|
199
|
+
return func
|
|
200
|
+
return deco
|
|
201
|
+
|
|
202
|
+
def on_poll_answer(self, filter=None, group: int = 0):
|
|
203
|
+
f = filter or filters.all
|
|
204
|
+
|
|
205
|
+
def deco(func):
|
|
206
|
+
self.dispatcher.add_handler(PollAnswerHandler(func, f, group))
|
|
207
|
+
return func
|
|
208
|
+
return deco
|
|
209
|
+
|
|
210
|
+
def on_message_reaction(self, filter=None, group: int = 0):
|
|
211
|
+
f = filter or filters.all
|
|
212
|
+
|
|
213
|
+
def deco(func):
|
|
214
|
+
self.dispatcher.add_handler(MessageReactionHandler(func, f, group))
|
|
215
|
+
return func
|
|
216
|
+
return deco
|
|
217
|
+
|
|
218
|
+
def on_raw_update(self, group: int = 0):
|
|
219
|
+
def deco(func):
|
|
220
|
+
self.dispatcher.add_handler(RawUpdateHandler(func, filters.all, group))
|
|
221
|
+
return func
|
|
222
|
+
return deco
|
|
223
|
+
|
|
224
|
+
# =================== Middleware ===================
|
|
225
|
+
def add_middleware(self, middleware, group: int = 0):
|
|
226
|
+
self.dispatcher.add_middleware(middleware, group)
|
|
227
|
+
|
|
228
|
+
# =================== HTTP ===================
|
|
229
|
+
async def _ensure_http(self):
|
|
230
|
+
if self._http is None:
|
|
231
|
+
self._http = await get_session()
|
|
232
|
+
return self._http
|
|
233
|
+
|
|
234
|
+
async def _request(self, method: str, **params):
|
|
235
|
+
sess = await self._ensure_http()
|
|
236
|
+
url = self.api_url.format(token=self.bot_token, method=method)
|
|
237
|
+
payload = {k: v for k, v in params.items() if v is not None}
|
|
238
|
+
|
|
239
|
+
for attempt in range(2):
|
|
240
|
+
async with self._sem:
|
|
241
|
+
async with sess.post(url, json=payload, proxy=self.proxy) as resp:
|
|
242
|
+
data = await resp.json(content_type=None)
|
|
243
|
+
|
|
244
|
+
if data.get("ok"):
|
|
245
|
+
return data["result"]
|
|
246
|
+
|
|
247
|
+
err = map_error(
|
|
248
|
+
data.get("error_code", 400),
|
|
249
|
+
data.get("description", "unknown error"),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
self.flood_retry
|
|
254
|
+
and getattr(err, "seconds", None)
|
|
255
|
+
and attempt == 0
|
|
256
|
+
):
|
|
257
|
+
log.warning(
|
|
258
|
+
"FloodWait %ss — retry %s", err.seconds, method
|
|
259
|
+
)
|
|
260
|
+
await asyncio.sleep(err.seconds + 1)
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
raise err
|
|
264
|
+
|
|
265
|
+
raise RuntimeError(f"Request {method} gagal setelah retry")
|
|
266
|
+
|
|
267
|
+
# =================== Polling ===================
|
|
268
|
+
async def _polling_loop(self):
|
|
269
|
+
while self._running:
|
|
270
|
+
try:
|
|
271
|
+
updates = await self._request(
|
|
272
|
+
"getUpdates",
|
|
273
|
+
offset=self._offset,
|
|
274
|
+
timeout=30,
|
|
275
|
+
allowed_updates=ALLOWED_UPDATES,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
for u in updates:
|
|
279
|
+
self._offset = u["update_id"] + 1
|
|
280
|
+
self.session.set("offset", self._offset)
|
|
281
|
+
await self.dispatcher.feed(u)
|
|
282
|
+
|
|
283
|
+
except asyncio.CancelledError:
|
|
284
|
+
break
|
|
285
|
+
except Exception as e:
|
|
286
|
+
log.exception("Polling error: %s", e)
|
|
287
|
+
await asyncio.sleep(3)
|
|
288
|
+
|
|
289
|
+
# =================== Lifecycle ===================
|
|
290
|
+
async def start(self):
|
|
291
|
+
"""Mulai bot: login, ambil info, jalankan polling."""
|
|
292
|
+
if self._running:
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
await self._ensure_http()
|
|
296
|
+
self.me = await self.get_me()
|
|
297
|
+
log.info("Bot started: @%s (id=%s)",
|
|
298
|
+
self.me.get("username"), self.me.get("id"))
|
|
299
|
+
|
|
300
|
+
self._running = True
|
|
301
|
+
self._poll_task = asyncio.create_task(self._polling_loop())
|
|
302
|
+
|
|
303
|
+
async def stop(self):
|
|
304
|
+
"""Hentikan bot."""
|
|
305
|
+
self._running = False
|
|
306
|
+
|
|
307
|
+
if self._poll_task:
|
|
308
|
+
self._poll_task.cancel()
|
|
309
|
+
try:
|
|
310
|
+
await self._poll_task
|
|
311
|
+
except asyncio.CancelledError:
|
|
312
|
+
pass
|
|
313
|
+
self._poll_task = None
|
|
314
|
+
|
|
315
|
+
log.info("Bot stopped")
|
|
316
|
+
|
|
317
|
+
async def idle(self):
|
|
318
|
+
from .idle import idle as _idle
|
|
319
|
+
try:
|
|
320
|
+
await _idle()
|
|
321
|
+
finally:
|
|
322
|
+
await self.stop()
|
|
323
|
+
|
|
324
|
+
def run(self):
|
|
325
|
+
"""
|
|
326
|
+
Blocking entry point — mirip pyrogram.Client.run().
|
|
327
|
+
|
|
328
|
+
Menjalankan start(), menahan sampai Ctrl+C, lalu stop().
|
|
329
|
+
"""
|
|
330
|
+
async def _main():
|
|
331
|
+
await self.start()
|
|
332
|
+
try:
|
|
333
|
+
await self.idle()
|
|
334
|
+
except KeyboardInterrupt:
|
|
335
|
+
await self.stop()
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
asyncio.run(_main())
|
|
339
|
+
finally:
|
|
340
|
+
# Tutup pool aiohttp global (kalau tidak ada client lain)
|
|
341
|
+
try:
|
|
342
|
+
loop = asyncio.new_event_loop()
|
|
343
|
+
loop.run_until_complete(close_pool())
|
|
344
|
+
loop.close()
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
async def __aenter__(self):
|
|
349
|
+
await self.start()
|
|
350
|
+
return self
|
|
351
|
+
|
|
352
|
+
async def __aexit__(self, *exc):
|
|
353
|
+
await self.stop()
|
pyrogram/dispatcher.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dispatcher: menerima update dari polling/webhook dan menyalurkannya
|
|
3
|
+
ke handler sesuai urutan `group`.
|
|
4
|
+
|
|
5
|
+
Middleware bisa disisipkan dengan `add_middleware(mw, group=...)`.
|
|
6
|
+
Middleware berjalan sebelum handler, bisa memanggil `await next()` untuk
|
|
7
|
+
melanjutkan, atau tidak memanggilnya untuk menggugurkan update.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from .errors import RPCError
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Dispatcher:
|
|
19
|
+
def __init__(self, client):
|
|
20
|
+
self.client = client
|
|
21
|
+
self.handlers = []
|
|
22
|
+
self.middlewares = []
|
|
23
|
+
self._sem = asyncio.Semaphore(max(4, client.workers))
|
|
24
|
+
|
|
25
|
+
def add_handler(self, handler):
|
|
26
|
+
self.handlers.append(handler)
|
|
27
|
+
self.handlers.sort(key=lambda h: h.group)
|
|
28
|
+
|
|
29
|
+
def add_middleware(self, middleware, group=0):
|
|
30
|
+
self.middlewares.append((group, middleware))
|
|
31
|
+
self.middlewares.sort(key=lambda x: x[0])
|
|
32
|
+
|
|
33
|
+
async def feed(self, update: dict):
|
|
34
|
+
"""Terima update — dispatch di task terpisah, dibatasi semaphore."""
|
|
35
|
+
async with self._sem:
|
|
36
|
+
asyncio.create_task(self._dispatch(update))
|
|
37
|
+
|
|
38
|
+
async def _dispatch(self, update: dict):
|
|
39
|
+
async def terminal():
|
|
40
|
+
for handler in self.handlers:
|
|
41
|
+
try:
|
|
42
|
+
if await handler.check(self.client, update):
|
|
43
|
+
await handler.handle(self.client, update)
|
|
44
|
+
except RPCError as e:
|
|
45
|
+
log.warning("RPC error di handler %r: %s", handler, e)
|
|
46
|
+
except Exception:
|
|
47
|
+
log.exception("Handler error di %r", handler)
|
|
48
|
+
|
|
49
|
+
async def run_chain(i=0):
|
|
50
|
+
if i >= len(self.middlewares):
|
|
51
|
+
await terminal()
|
|
52
|
+
return
|
|
53
|
+
_, mw = self.middlewares[i]
|
|
54
|
+
|
|
55
|
+
async def nxt():
|
|
56
|
+
await run_chain(i + 1)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
await mw(self.client, update, nxt)
|
|
60
|
+
except Exception:
|
|
61
|
+
log.exception("Middleware error di %r", mw)
|
|
62
|
+
|
|
63
|
+
await run_chain()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .chat_action import ChatAction
|
|
2
|
+
from .chat_member_status import ChatMemberStatus
|
|
3
|
+
from .chat_type import ChatType
|
|
4
|
+
from .message_entity_type import MessageEntityType
|
|
5
|
+
from .message_service_type import MessageServiceType
|
|
6
|
+
from .parse_mode import ParseMode
|
|
7
|
+
from .update_type import UpdateType, ALLOWED_UPDATES
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ChatAction",
|
|
11
|
+
"ChatMemberStatus",
|
|
12
|
+
"ChatType",
|
|
13
|
+
"MessageEntityType",
|
|
14
|
+
"MessageServiceType",
|
|
15
|
+
"ParseMode",
|
|
16
|
+
"UpdateType",
|
|
17
|
+
"ALLOWED_UPDATES",
|
|
18
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ChatAction(str, Enum):
|
|
5
|
+
TYPING = "typing"
|
|
6
|
+
UPLOAD_PHOTO = "upload_photo"
|
|
7
|
+
RECORD_VIDEO = "record_video"
|
|
8
|
+
UPLOAD_VIDEO = "upload_video"
|
|
9
|
+
RECORD_VOICE = "record_voice"
|
|
10
|
+
UPLOAD_VOICE = "upload_voice"
|
|
11
|
+
UPLOAD_DOCUMENT = "upload_document"
|
|
12
|
+
CHOOSE_STICKER = "choose_sticker"
|
|
13
|
+
FIND_LOCATION = "find_location"
|
|
14
|
+
RECORD_VIDEO_NOTE = "record_video_note"
|
|
15
|
+
UPLOAD_VIDEO_NOTE = "upload_video_note"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MessageEntityType(str, Enum):
|
|
5
|
+
MENTION = "mention"
|
|
6
|
+
HASHTAG = "hashtag"
|
|
7
|
+
CASHTAG = "cashtag"
|
|
8
|
+
BOT_COMMAND = "bot_command"
|
|
9
|
+
URL = "url"
|
|
10
|
+
EMAIL = "email"
|
|
11
|
+
PHONE_NUMBER = "phone_number"
|
|
12
|
+
BOLD = "bold"
|
|
13
|
+
ITALIC = "italic"
|
|
14
|
+
UNDERLINE = "underline"
|
|
15
|
+
STRIKETHROUGH = "strikethrough"
|
|
16
|
+
SPOILER = "spoiler"
|
|
17
|
+
BLOCKQUOTE = "blockquote"
|
|
18
|
+
EXPANDABLE_BLOCKQUOTE = "expandable_blockquote"
|
|
19
|
+
CODE = "code"
|
|
20
|
+
PRE = "pre"
|
|
21
|
+
TEXT_LINK = "text_link"
|
|
22
|
+
TEXT_MENTION = "text_mention"
|
|
23
|
+
CUSTOM_EMOJI = "custom_emoji"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MessageServiceType(str, Enum):
|
|
5
|
+
NEW_CHAT_MEMBERS = "new_chat_members"
|
|
6
|
+
LEFT_CHAT_MEMBER = "left_chat_member"
|
|
7
|
+
NEW_CHAT_TITLE = "new_chat_title"
|
|
8
|
+
NEW_CHAT_PHOTO = "new_chat_photo"
|
|
9
|
+
DELETE_CHAT_PHOTO = "delete_chat_photo"
|
|
10
|
+
GROUP_CHAT_CREATED = "group_chat_created"
|
|
11
|
+
SUPERGROUP_CHAT_CREATED = "supergroup_chat_created"
|
|
12
|
+
CHANNEL_CHAT_CREATED = "channel_chat_created"
|
|
13
|
+
MESSAGE_AUTO_DELETE_TIMER_CHANGED = "message_auto_delete_timer_changed"
|
|
14
|
+
MIGRATE_TO_CHAT_ID = "migrate_to_chat_id"
|
|
15
|
+
MIGRATE_FROM_CHAT_ID = "migrate_from_chat_id"
|
|
16
|
+
PINNED_MESSAGE = "pinned_message"
|
|
17
|
+
FORUM_TOPIC_CREATED = "forum_topic_created"
|
|
18
|
+
FORUM_TOPIC_EDITED = "forum_topic_edited"
|
|
19
|
+
FORUM_TOPIC_CLOSED = "forum_topic_closed"
|
|
20
|
+
FORUM_TOPIC_REOPENED = "forum_topic_reopened"
|
|
21
|
+
GENERAL_FORUM_TOPIC_HIDDEN = "general_forum_topic_hidden"
|
|
22
|
+
GENERAL_FORUM_TOPIC_UNHIDDEN = "general_forum_topic_unhidden"
|
|
23
|
+
VIDEO_CHAT_SCHEDULED = "video_chat_scheduled"
|
|
24
|
+
VIDEO_CHAT_STARTED = "video_chat_started"
|
|
25
|
+
VIDEO_CHAT_ENDED = "video_chat_ended"
|
|
26
|
+
VIDEO_CHAT_PARTICIPANTS_INVITED = "video_chat_participants_invited"
|
|
27
|
+
WEB_APP_DATA = "web_app_data"
|
|
28
|
+
BOOST_ADDED = "boost_added"
|
|
29
|
+
CHAT_BACKGROUND_SET = "chat_background_set"
|
|
30
|
+
WRITE_ACCESS_ALLOWED = "write_access_allowed"
|
|
31
|
+
PROXIMITY_ALERT_TRIGGERED = "proximity_alert_triggered"
|
|
32
|
+
GIVEAWAY_CREATED = "giveaway_created"
|
|
33
|
+
GIVEAWAY_COMPLETED = "giveaway_completed"
|
|
34
|
+
GIVEAWAY_WINNERS = "giveaway_winners"
|
|
35
|
+
CHECKLIST = "checklist"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UpdateType(str, Enum):
|
|
5
|
+
MESSAGE = "message"
|
|
6
|
+
EDITED_MESSAGE = "edited_message"
|
|
7
|
+
CHANNEL_POST = "channel_post"
|
|
8
|
+
EDITED_CHANNEL_POST = "edited_channel_post"
|
|
9
|
+
BUSINESS_CONNECTION = "business_connection"
|
|
10
|
+
BUSINESS_MESSAGE = "business_message"
|
|
11
|
+
EDITED_BUSINESS_MESSAGE = "edited_business_message"
|
|
12
|
+
DELETED_BUSINESS_MESSAGES = "deleted_business_messages"
|
|
13
|
+
MESSAGE_REACTION = "message_reaction"
|
|
14
|
+
MESSAGE_REACTION_COUNT = "message_reaction_count"
|
|
15
|
+
INLINE_QUERY = "inline_query"
|
|
16
|
+
CHOSEN_INLINE_RESULT = "chosen_inline_result"
|
|
17
|
+
CALLBACK_QUERY = "callback_query"
|
|
18
|
+
SHIPPING_QUERY = "shipping_query"
|
|
19
|
+
PRE_CHECKOUT_QUERY = "pre_checkout_query"
|
|
20
|
+
PURCHASED_PAID_MEDIA = "purchased_paid_media"
|
|
21
|
+
POLL = "poll"
|
|
22
|
+
POLL_ANSWER = "poll_answer"
|
|
23
|
+
MY_CHAT_MEMBER = "my_chat_member"
|
|
24
|
+
CHAT_MEMBER = "chat_member"
|
|
25
|
+
CHAT_JOIN_REQUEST = "chat_join_request"
|
|
26
|
+
CHAT_BOOST = "chat_boost"
|
|
27
|
+
REMOVED_CHAT_BOOST = "removed_chat_boost"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
ALLOWED_UPDATES = [u.value for u in UpdateType]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .rpc_error import RPCError
|
|
2
|
+
from .bad_request import BadRequest
|
|
3
|
+
from .unauthorized import Unauthorized
|
|
4
|
+
from .forbidden import Forbidden
|
|
5
|
+
from .not_found import NotFound
|
|
6
|
+
from .conflict import Conflict
|
|
7
|
+
from .internal_server_error import InternalServerError
|
|
8
|
+
from .service_unavailable import ServiceUnavailable
|
|
9
|
+
from .flood_wait import FloodWait
|
|
10
|
+
from .message_not_modified import MessageNotModified
|
|
11
|
+
from .map_error import map_error
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"RPCError",
|
|
15
|
+
"BadRequest",
|
|
16
|
+
"Unauthorized",
|
|
17
|
+
"Forbidden",
|
|
18
|
+
"NotFound",
|
|
19
|
+
"Conflict",
|
|
20
|
+
"InternalServerError",
|
|
21
|
+
"ServiceUnavailable",
|
|
22
|
+
"FloodWait",
|
|
23
|
+
"MessageNotModified",
|
|
24
|
+
"map_error",
|
|
25
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .rpc_error import RPCError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FloodWait(RPCError):
|
|
5
|
+
"""
|
|
6
|
+
Bot harus menunggu `seconds` detik sebelum request berikutnya.
|
|
7
|
+
Dilempar saat Telegram mengembalikan 429 dengan "retry after".
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, seconds, description=None):
|
|
11
|
+
super().__init__(f"Flood wait: {seconds}s", code=429, description=description)
|
|
12
|
+
self.seconds = seconds
|