This commit is contained in:
flaskfarm
2022-10-06 14:40:55 +09:00
parent b4e737a6b2
commit 4b72b7dc65
18 changed files with 6668 additions and 0 deletions

View File

@@ -0,0 +1,926 @@
import io
import json
import time
import asyncio
import traceback
import collections
from concurrent.futures._base import CancelledError
from . import helper, api
from .. import (
_BotBase, flavor, _find_first_key, _isstring, _strip, _rectify,
_dismantle_message_identifier, _split_input_media_array
)
# Patch aiohttp for sending unicode filename
from . import hack
from .. import exception
def flavor_router(routing_table):
router = helper.Router(flavor, routing_table)
return router.route
class Bot(_BotBase):
class Scheduler(object):
def __init__(self, loop):
self._loop = loop
self._callback = None
def on_event(self, callback):
self._callback = callback
def event_at(self, when, data):
delay = when - time.time()
return self._loop.call_later(delay, self._callback, data)
# call_at() uses event loop time, not unix time.
# May as well use call_later here.
def event_later(self, delay, data):
return self._loop.call_later(delay, self._callback, data)
def event_now(self, data):
return self._loop.call_soon(self._callback, data)
def cancel(self, event):
return event.cancel()
def __init__(self, token, loop=None):
super(Bot, self).__init__(token)
self._loop = loop or asyncio.get_event_loop()
api._loop = self._loop # sync loop with api module
self._scheduler = self.Scheduler(self._loop)
self._router = helper.Router(flavor, {'chat': helper._create_invoker(self, 'on_chat_message'),
'callback_query': helper._create_invoker(self, 'on_callback_query'),
'inline_query': helper._create_invoker(self, 'on_inline_query'),
'chosen_inline_result': helper._create_invoker(self, 'on_chosen_inline_result')})
@property
def loop(self):
return self._loop
@property
def scheduler(self):
return self._scheduler
@property
def router(self):
return self._router
async def handle(self, msg):
await self._router.route(msg)
async def _api_request(self, method, params=None, files=None, **kwargs):
return await api.request((self._token, method, params, files), **kwargs)
async def _api_request_with_file(self, method, params, file_key, file_value, **kwargs):
if _isstring(file_value):
params[file_key] = file_value
return await self._api_request(method, _rectify(params), **kwargs)
else:
files = {file_key: file_value}
return await self._api_request(method, _rectify(params), files, **kwargs)
async def getMe(self):
""" See: https://core.telegram.org/bots/api#getme """
return await self._api_request('getMe')
async def sendMessage(self, chat_id, text,
parse_mode=None,
disable_web_page_preview=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
""" See: https://core.telegram.org/bots/api#sendmessage """
p = _strip(locals())
return await self._api_request('sendMessage', _rectify(p))
async def forwardMessage(self, chat_id, from_chat_id, message_id,
disable_notification=None):
""" See: https://core.telegram.org/bots/api#forwardmessage """
p = _strip(locals())
return await self._api_request('forwardMessage', _rectify(p))
async def sendPhoto(self, chat_id, photo,
caption=None,
parse_mode=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#sendphoto
:param photo:
- string: ``file_id`` for a photo existing on Telegram servers
- string: HTTP URL of a photo from the Internet
- file-like object: obtained by ``open(path, 'rb')``
- tuple: (filename, file-like object). If the filename contains
non-ASCII characters and you are using Python 2.7, make sure the
filename is a unicode string.
"""
p = _strip(locals(), more=['photo'])
return await self._api_request_with_file('sendPhoto', _rectify(p), 'photo', photo)
async def sendAudio(self, chat_id, audio,
caption=None,
parse_mode=None,
duration=None,
performer=None,
title=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#sendaudio
:param audio: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
"""
p = _strip(locals(), more=['audio'])
return await self._api_request_with_file('sendAudio', _rectify(p), 'audio', audio)
async def sendDocument(self, chat_id, document,
caption=None,
parse_mode=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#senddocument
:param document: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
"""
p = _strip(locals(), more=['document'])
return await self._api_request_with_file('sendDocument', _rectify(p), 'document', document)
async def sendVideo(self, chat_id, video,
duration=None,
width=None,
height=None,
caption=None,
parse_mode=None,
supports_streaming=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#sendvideo
:param video: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
"""
p = _strip(locals(), more=['video'])
return await self._api_request_with_file('sendVideo', _rectify(p), 'video', video)
async def sendVoice(self, chat_id, voice,
caption=None,
parse_mode=None,
duration=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#sendvoice
:param voice: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
"""
p = _strip(locals(), more=['voice'])
return await self._api_request_with_file('sendVoice', _rectify(p), 'voice', voice)
async def sendVideoNote(self, chat_id, video_note,
duration=None,
length=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#sendvideonote
:param voice: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
:param length:
Although marked as optional, this method does not seem to work without
it being specified. Supply any integer you want. It seems to have no effect
on the video note's display size.
"""
p = _strip(locals(), more=['video_note'])
return await self._api_request_with_file('sendVideoNote', _rectify(p), 'video_note', video_note)
async def sendMediaGroup(self, chat_id, media,
disable_notification=None,
reply_to_message_id=None):
"""
See: https://core.telegram.org/bots/api#sendmediagroup
:type media: array of `InputMedia <https://core.telegram.org/bots/api#inputmedia>`_ objects
:param media:
To indicate media locations, each InputMedia object's ``media`` field
should be one of these:
- string: ``file_id`` for a file existing on Telegram servers
- string: HTTP URL of a file from the Internet
- file-like object: obtained by ``open(path, 'rb')``
- tuple: (form-data name, file-like object)
- tuple: (form-data name, (filename, file-like object))
In case of uploading, you may supply customized multipart/form-data
names for each uploaded file (as in last 2 options above). Otherwise,
telepot assigns unique names to each uploaded file. Names assigned by
telepot will not collide with user-supplied names, if any.
"""
p = _strip(locals(), more=['media'])
legal_media, files_to_attach = _split_input_media_array(media)
p['media'] = legal_media
return await self._api_request('sendMediaGroup', _rectify(p), files_to_attach)
async def sendLocation(self, chat_id, latitude, longitude,
live_period=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
""" See: https://core.telegram.org/bots/api#sendlocation """
p = _strip(locals())
return await self._api_request('sendLocation', _rectify(p))
async def editMessageLiveLocation(self, msg_identifier, latitude, longitude,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#editmessagelivelocation
:param msg_identifier: Same as in :meth:`.Bot.editMessageText`
"""
p = _strip(locals(), more=['msg_identifier'])
p.update(_dismantle_message_identifier(msg_identifier))
return await self._api_request('editMessageLiveLocation', _rectify(p))
async def stopMessageLiveLocation(self, msg_identifier,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#stopmessagelivelocation
:param msg_identifier: Same as in :meth:`.Bot.editMessageText`
"""
p = _strip(locals(), more=['msg_identifier'])
p.update(_dismantle_message_identifier(msg_identifier))
return await self._api_request('stopMessageLiveLocation', _rectify(p))
async def sendVenue(self, chat_id, latitude, longitude, title, address,
foursquare_id=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
""" See: https://core.telegram.org/bots/api#sendvenue """
p = _strip(locals())
return await self._api_request('sendVenue', _rectify(p))
async def sendContact(self, chat_id, phone_number, first_name,
last_name=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
""" See: https://core.telegram.org/bots/api#sendcontact """
p = _strip(locals())
return await self._api_request('sendContact', _rectify(p))
async def sendGame(self, chat_id, game_short_name,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
""" See: https://core.telegram.org/bots/api#sendgame """
p = _strip(locals())
return await self._api_request('sendGame', _rectify(p))
async def sendInvoice(self, chat_id, title, description, payload,
provider_token, start_parameter, currency, prices,
provider_data=None,
photo_url=None,
photo_size=None,
photo_width=None,
photo_height=None,
need_name=None,
need_phone_number=None,
need_email=None,
need_shipping_address=None,
is_flexible=None,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
""" See: https://core.telegram.org/bots/api#sendinvoice """
p = _strip(locals())
return await self._api_request('sendInvoice', _rectify(p))
async def sendChatAction(self, chat_id, action):
""" See: https://core.telegram.org/bots/api#sendchataction """
p = _strip(locals())
return await self._api_request('sendChatAction', _rectify(p))
async def getUserProfilePhotos(self, user_id,
offset=None,
limit=None):
""" See: https://core.telegram.org/bots/api#getuserprofilephotos """
p = _strip(locals())
return await self._api_request('getUserProfilePhotos', _rectify(p))
async def getFile(self, file_id):
""" See: https://core.telegram.org/bots/api#getfile """
p = _strip(locals())
return await self._api_request('getFile', _rectify(p))
async def kickChatMember(self, chat_id, user_id,
until_date=None):
""" See: https://core.telegram.org/bots/api#kickchatmember """
p = _strip(locals())
return await self._api_request('kickChatMember', _rectify(p))
async def unbanChatMember(self, chat_id, user_id):
""" See: https://core.telegram.org/bots/api#unbanchatmember """
p = _strip(locals())
return await self._api_request('unbanChatMember', _rectify(p))
async def restrictChatMember(self, chat_id, user_id,
until_date=None,
can_send_messages=None,
can_send_media_messages=None,
can_send_other_messages=None,
can_add_web_page_previews=None):
""" See: https://core.telegram.org/bots/api#restrictchatmember """
p = _strip(locals())
return await self._api_request('restrictChatMember', _rectify(p))
async def promoteChatMember(self, chat_id, user_id,
can_change_info=None,
can_post_messages=None,
can_edit_messages=None,
can_delete_messages=None,
can_invite_users=None,
can_restrict_members=None,
can_pin_messages=None,
can_promote_members=None):
""" See: https://core.telegram.org/bots/api#promotechatmember """
p = _strip(locals())
return await self._api_request('promoteChatMember', _rectify(p))
async def exportChatInviteLink(self, chat_id):
""" See: https://core.telegram.org/bots/api#exportchatinvitelink """
p = _strip(locals())
return await self._api_request('exportChatInviteLink', _rectify(p))
async def setChatPhoto(self, chat_id, photo):
""" See: https://core.telegram.org/bots/api#setchatphoto """
p = _strip(locals(), more=['photo'])
return await self._api_request_with_file('setChatPhoto', _rectify(p), 'photo', photo)
async def deleteChatPhoto(self, chat_id):
""" See: https://core.telegram.org/bots/api#deletechatphoto """
p = _strip(locals())
return await self._api_request('deleteChatPhoto', _rectify(p))
async def setChatTitle(self, chat_id, title):
""" See: https://core.telegram.org/bots/api#setchattitle """
p = _strip(locals())
return await self._api_request('setChatTitle', _rectify(p))
async def setChatDescription(self, chat_id,
description=None):
""" See: https://core.telegram.org/bots/api#setchatdescription """
p = _strip(locals())
return await self._api_request('setChatDescription', _rectify(p))
async def pinChatMessage(self, chat_id, message_id,
disable_notification=None):
""" See: https://core.telegram.org/bots/api#pinchatmessage """
p = _strip(locals())
return await self._api_request('pinChatMessage', _rectify(p))
async def unpinChatMessage(self, chat_id):
""" See: https://core.telegram.org/bots/api#unpinchatmessage """
p = _strip(locals())
return await self._api_request('unpinChatMessage', _rectify(p))
async def leaveChat(self, chat_id):
""" See: https://core.telegram.org/bots/api#leavechat """
p = _strip(locals())
return await self._api_request('leaveChat', _rectify(p))
async def getChat(self, chat_id):
""" See: https://core.telegram.org/bots/api#getchat """
p = _strip(locals())
return await self._api_request('getChat', _rectify(p))
async def getChatAdministrators(self, chat_id):
""" See: https://core.telegram.org/bots/api#getchatadministrators """
p = _strip(locals())
return await self._api_request('getChatAdministrators', _rectify(p))
async def getChatMembersCount(self, chat_id):
""" See: https://core.telegram.org/bots/api#getchatmemberscount """
p = _strip(locals())
return await self._api_request('getChatMembersCount', _rectify(p))
async def getChatMember(self, chat_id, user_id):
""" See: https://core.telegram.org/bots/api#getchatmember """
p = _strip(locals())
return await self._api_request('getChatMember', _rectify(p))
async def setChatStickerSet(self, chat_id, sticker_set_name):
""" See: https://core.telegram.org/bots/api#setchatstickerset """
p = _strip(locals())
return await self._api_request('setChatStickerSet', _rectify(p))
async def deleteChatStickerSet(self, chat_id):
""" See: https://core.telegram.org/bots/api#deletechatstickerset """
p = _strip(locals())
return await self._api_request('deleteChatStickerSet', _rectify(p))
async def answerCallbackQuery(self, callback_query_id,
text=None,
show_alert=None,
url=None,
cache_time=None):
""" See: https://core.telegram.org/bots/api#answercallbackquery """
p = _strip(locals())
return await self._api_request('answerCallbackQuery', _rectify(p))
async def answerShippingQuery(self, shipping_query_id, ok,
shipping_options=None,
error_message=None):
""" See: https://core.telegram.org/bots/api#answershippingquery """
p = _strip(locals())
return await self._api_request('answerShippingQuery', _rectify(p))
async def answerPreCheckoutQuery(self, pre_checkout_query_id, ok,
error_message=None):
""" See: https://core.telegram.org/bots/api#answerprecheckoutquery """
p = _strip(locals())
return await self._api_request('answerPreCheckoutQuery', _rectify(p))
async def editMessageText(self, msg_identifier, text,
parse_mode=None,
disable_web_page_preview=None,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#editmessagetext
:param msg_identifier:
a 2-tuple (``chat_id``, ``message_id``),
a 1-tuple (``inline_message_id``),
or simply ``inline_message_id``.
You may extract this value easily with :meth:`telepot.message_identifier`
"""
p = _strip(locals(), more=['msg_identifier'])
p.update(_dismantle_message_identifier(msg_identifier))
return await self._api_request('editMessageText', _rectify(p))
async def editMessageCaption(self, msg_identifier,
caption=None,
parse_mode=None,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#editmessagecaption
:param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.aio.Bot.editMessageText`
"""
p = _strip(locals(), more=['msg_identifier'])
p.update(_dismantle_message_identifier(msg_identifier))
return await self._api_request('editMessageCaption', _rectify(p))
async def editMessageReplyMarkup(self, msg_identifier,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#editmessagereplymarkup
:param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.aio.Bot.editMessageText`
"""
p = _strip(locals(), more=['msg_identifier'])
p.update(_dismantle_message_identifier(msg_identifier))
return await self._api_request('editMessageReplyMarkup', _rectify(p))
async def deleteMessage(self, msg_identifier):
"""
See: https://core.telegram.org/bots/api#deletemessage
:param msg_identifier:
Same as ``msg_identifier`` in :meth:`telepot.aio.Bot.editMessageText`,
except this method does not work on inline messages.
"""
p = _strip(locals(), more=['msg_identifier'])
p.update(_dismantle_message_identifier(msg_identifier))
return await self._api_request('deleteMessage', _rectify(p))
async def sendSticker(self, chat_id, sticker,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None):
"""
See: https://core.telegram.org/bots/api#sendsticker
:param sticker: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
"""
p = _strip(locals(), more=['sticker'])
return await self._api_request_with_file('sendSticker', _rectify(p), 'sticker', sticker)
async def getStickerSet(self, name):
"""
See: https://core.telegram.org/bots/api#getstickerset
"""
p = _strip(locals())
return await self._api_request('getStickerSet', _rectify(p))
async def uploadStickerFile(self, user_id, png_sticker):
"""
See: https://core.telegram.org/bots/api#uploadstickerfile
"""
p = _strip(locals(), more=['png_sticker'])
return await self._api_request_with_file('uploadStickerFile', _rectify(p), 'png_sticker', png_sticker)
async def createNewStickerSet(self, user_id, name, title, png_sticker, emojis,
contains_masks=None,
mask_position=None):
"""
See: https://core.telegram.org/bots/api#createnewstickerset
"""
p = _strip(locals(), more=['png_sticker'])
return await self._api_request_with_file('createNewStickerSet', _rectify(p), 'png_sticker', png_sticker)
async def addStickerToSet(self, user_id, name, png_sticker, emojis,
mask_position=None):
"""
See: https://core.telegram.org/bots/api#addstickertoset
"""
p = _strip(locals(), more=['png_sticker'])
return await self._api_request_with_file('addStickerToSet', _rectify(p), 'png_sticker', png_sticker)
async def setStickerPositionInSet(self, sticker, position):
"""
See: https://core.telegram.org/bots/api#setstickerpositioninset
"""
p = _strip(locals())
return await self._api_request('setStickerPositionInSet', _rectify(p))
async def deleteStickerFromSet(self, sticker):
"""
See: https://core.telegram.org/bots/api#deletestickerfromset
"""
p = _strip(locals())
return await self._api_request('deleteStickerFromSet', _rectify(p))
async def answerInlineQuery(self, inline_query_id, results,
cache_time=None,
is_personal=None,
next_offset=None,
switch_pm_text=None,
switch_pm_parameter=None):
""" See: https://core.telegram.org/bots/api#answerinlinequery """
p = _strip(locals())
return await self._api_request('answerInlineQuery', _rectify(p))
async def getUpdates(self,
offset=None,
limit=None,
timeout=None,
allowed_updates=None):
""" See: https://core.telegram.org/bots/api#getupdates """
p = _strip(locals())
return await self._api_request('getUpdates', _rectify(p))
async def setWebhook(self,
url=None,
certificate=None,
max_connections=None,
allowed_updates=None):
""" See: https://core.telegram.org/bots/api#setwebhook """
p = _strip(locals(), more=['certificate'])
if certificate:
files = {'certificate': certificate}
return await self._api_request('setWebhook', _rectify(p), files)
else:
return await self._api_request('setWebhook', _rectify(p))
async def deleteWebhook(self):
""" See: https://core.telegram.org/bots/api#deletewebhook """
return await self._api_request('deleteWebhook')
async def getWebhookInfo(self):
""" See: https://core.telegram.org/bots/api#getwebhookinfo """
return await self._api_request('getWebhookInfo')
async def setGameScore(self, user_id, score, game_message_identifier,
force=None,
disable_edit_message=None):
""" See: https://core.telegram.org/bots/api#setgamescore """
p = _strip(locals(), more=['game_message_identifier'])
p.update(_dismantle_message_identifier(game_message_identifier))
return await self._api_request('setGameScore', _rectify(p))
async def getGameHighScores(self, user_id, game_message_identifier):
""" See: https://core.telegram.org/bots/api#getgamehighscores """
p = _strip(locals(), more=['game_message_identifier'])
p.update(_dismantle_message_identifier(game_message_identifier))
return await self._api_request('getGameHighScores', _rectify(p))
async def download_file(self, file_id, dest):
"""
Download a file to local disk.
:param dest: a path or a ``file`` object
"""
f = await self.getFile(file_id)
try:
d = dest if isinstance(dest, io.IOBase) else open(dest, 'wb')
session, request = api.download((self._token, f['file_path']))
async with session:
async with request as r:
while 1:
chunk = await r.content.read(self._file_chunk_size)
if not chunk:
break
d.write(chunk)
d.flush()
finally:
if not isinstance(dest, io.IOBase) and 'd' in locals():
d.close()
async def message_loop(self, handler=None, relax=0.1,
timeout=20, allowed_updates=None,
source=None, ordered=True, maxhold=3):
"""
Return a task to constantly ``getUpdates`` or pull updates from a queue.
Apply ``handler`` to every message received.
:param handler:
a function that takes one argument (the message), or a routing table.
If ``None``, the bot's ``handle`` method is used.
A *routing table* is a dictionary of ``{flavor: function}``, mapping messages to appropriate
handler functions according to their flavors. It allows you to define functions specifically
to handle one flavor of messages. It usually looks like this: ``{'chat': fn1,
'callback_query': fn2, 'inline_query': fn3, ...}``. Each handler function should take
one argument (the message).
:param source:
Source of updates.
If ``None``, ``getUpdates`` is used to obtain new messages from Telegram servers.
If it is a ``asyncio.Queue``, new messages are pulled from the queue.
A web application implementing a webhook can dump updates into the queue,
while the bot pulls from it. This is how telepot can be integrated with webhooks.
Acceptable contents in queue:
- ``str`` or ``bytes`` (decoded using UTF-8)
representing a JSON-serialized `Update <https://core.telegram.org/bots/api#update>`_ object.
- a ``dict`` representing an Update object.
When ``source`` is a queue, these parameters are meaningful:
:type ordered: bool
:param ordered:
If ``True``, ensure in-order delivery of messages to ``handler``
(i.e. updates with a smaller ``update_id`` always come before those with
a larger ``update_id``).
If ``False``, no re-ordering is done. ``handler`` is applied to messages
as soon as they are pulled from queue.
:type maxhold: float
:param maxhold:
Applied only when ``ordered`` is ``True``. The maximum number of seconds
an update is held waiting for a not-yet-arrived smaller ``update_id``.
When this number of seconds is up, the update is delivered to ``handler``
even if some smaller ``update_id``\s have not yet arrived. If those smaller
``update_id``\s arrive at some later time, they are discarded.
:type timeout: int
:param timeout:
``timeout`` parameter supplied to :meth:`telepot.aio.Bot.getUpdates`,
controlling how long to poll in seconds.
:type allowed_updates: array of string
:param allowed_updates:
``allowed_updates`` parameter supplied to :meth:`telepot.aio.Bot.getUpdates`,
controlling which types of updates to receive.
"""
if handler is None:
handler = self.handle
elif isinstance(handler, dict):
handler = flavor_router(handler)
def create_task_for(msg):
self.loop.create_task(handler(msg))
if asyncio.iscoroutinefunction(handler):
callback = create_task_for
else:
callback = handler
def handle(update):
try:
key = _find_first_key(update, ['message',
'edited_message',
'channel_post',
'edited_channel_post',
'callback_query',
'inline_query',
'chosen_inline_result',
'shipping_query',
'pre_checkout_query'])
callback(update[key])
except:
# Localize the error so message thread can keep going.
traceback.print_exc()
finally:
return update['update_id']
async def get_from_telegram_server():
offset = None # running offset
allowed_upd = allowed_updates
while 1:
try:
result = await self.getUpdates(offset=offset,
timeout=timeout,
allowed_updates=allowed_upd)
# Once passed, this parameter is no longer needed.
allowed_upd = None
if len(result) > 0:
# No sort. Trust server to give messages in correct order.
# Update offset to max(update_id) + 1
offset = max([handle(update) for update in result]) + 1
except CancelledError:
raise
except exception.BadHTTPResponse as e:
traceback.print_exc()
# Servers probably down. Wait longer.
if e.status == 502:
await asyncio.sleep(30)
except:
traceback.print_exc()
await asyncio.sleep(relax)
else:
await asyncio.sleep(relax)
def dictify(data):
if type(data) is bytes:
return json.loads(data.decode('utf-8'))
elif type(data) is str:
return json.loads(data)
elif type(data) is dict:
return data
else:
raise ValueError()
async def get_from_queue_unordered(qu):
while 1:
try:
data = await qu.get()
update = dictify(data)
handle(update)
except:
traceback.print_exc()
async def get_from_queue(qu):
# Here is the re-ordering mechanism, ensuring in-order delivery of updates.
max_id = None # max update_id passed to callback
buffer = collections.deque() # keep those updates which skip some update_id
qwait = None # how long to wait for updates,
# because buffer's content has to be returned in time.
while 1:
try:
data = await asyncio.wait_for(qu.get(), qwait)
update = dictify(data)
if max_id is None:
# First message received, handle regardless.
max_id = handle(update)
elif update['update_id'] == max_id + 1:
# No update_id skipped, handle naturally.
max_id = handle(update)
# clear contagious updates in buffer
if len(buffer) > 0:
buffer.popleft() # first element belongs to update just received, useless now.
while 1:
try:
if type(buffer[0]) is dict:
max_id = handle(buffer.popleft()) # updates that arrived earlier, handle them.
else:
break # gap, no more contagious updates
except IndexError:
break # buffer empty
elif update['update_id'] > max_id + 1:
# Update arrives pre-maturely, insert to buffer.
nbuf = len(buffer)
if update['update_id'] <= max_id + nbuf:
# buffer long enough, put update at position
buffer[update['update_id'] - max_id - 1] = update
else:
# buffer too short, lengthen it
expire = time.time() + maxhold
for a in range(nbuf, update['update_id']-max_id-1):
buffer.append(expire) # put expiry time in gaps
buffer.append(update)
else:
pass # discard
except asyncio.TimeoutError:
# debug message
# print('Timeout')
# some buffer contents have to be handled
# flush buffer until a non-expired time is encountered
while 1:
try:
if type(buffer[0]) is dict:
max_id = handle(buffer.popleft())
else:
expire = buffer[0]
if expire <= time.time():
max_id += 1
buffer.popleft()
else:
break # non-expired
except IndexError:
break # buffer empty
except:
traceback.print_exc()
finally:
try:
# don't wait longer than next expiry time
qwait = buffer[0] - time.time()
if qwait < 0:
qwait = 0
except IndexError:
# buffer empty, can wait forever
qwait = None
# debug message
# print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id)
self._scheduler._callback = callback
if source is None:
await get_from_telegram_server()
elif isinstance(source, asyncio.Queue):
if ordered:
await get_from_queue(source)
else:
await get_from_queue_unordered(source)
else:
raise ValueError('Invalid source')
class SpeakerBot(Bot):
def __init__(self, token, loop=None):
super(SpeakerBot, self).__init__(token, loop)
self._mic = helper.Microphone()
@property
def mic(self):
return self._mic
def create_listener(self):
q = asyncio.Queue()
self._mic.add(q)
ln = helper.Listener(self._mic, q)
return ln
class DelegatorBot(SpeakerBot):
def __init__(self, token, delegation_patterns, loop=None):
"""
:param delegation_patterns: a list of (seeder, delegator) tuples.
"""
super(DelegatorBot, self).__init__(token, loop)
self._delegate_records = [p+({},) for p in delegation_patterns]
def handle(self, msg):
self._mic.send(msg)
for calculate_seed, make_coroutine_obj, dict in self._delegate_records:
id = calculate_seed(msg)
if id is None:
continue
elif isinstance(id, collections.Hashable):
if id not in dict or dict[id].done():
c = make_coroutine_obj((self, msg, id))
if not asyncio.iscoroutine(c):
raise RuntimeError('You must produce a coroutine *object* as delegate.')
dict[id] = self._loop.create_task(c)
else:
c = make_coroutine_obj((self, msg, id))
self._loop.create_task(c)

View File

@@ -0,0 +1,168 @@
import asyncio
import aiohttp
import async_timeout
import atexit
import re
import json
from .. import exception
from ..api import _methodurl, _which_pool, _fileurl, _guess_filename
_loop = asyncio.get_event_loop()
_pools = {
'default': aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=10),
loop=_loop)
}
_timeout = 30
_proxy = None # (url, (username, password))
def set_proxy(url, basic_auth=None):
global _proxy
if not url:
_proxy = None
else:
_proxy = (url, basic_auth) if basic_auth else (url,)
def _proxy_kwargs():
if _proxy is None or len(_proxy) == 0:
return {}
elif len(_proxy) == 1:
return {'proxy': _proxy[0]}
elif len(_proxy) == 2:
return {'proxy': _proxy[0], 'proxy_auth': aiohttp.BasicAuth(*_proxy[1])}
else:
raise RuntimeError("_proxy has invalid length")
async def _close_pools():
global _pools
for s in _pools.values():
await s.close()
atexit.register(lambda: _loop.create_task(_close_pools())) # have to wrap async function
def _create_onetime_pool():
return aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=1, force_close=True),
loop=_loop)
def _default_timeout(req, **user_kw):
return _timeout
def _compose_timeout(req, **user_kw):
token, method, params, files = req
if method == 'getUpdates' and params and 'timeout' in params:
# Ensure HTTP timeout is longer than getUpdates timeout
return params['timeout'] + _default_timeout(req, **user_kw)
elif files:
# Disable timeout if uploading files. For some reason, the larger the file,
# the longer it takes for the server to respond (after upload is finished).
# It is unclear how long timeout should be.
return None
else:
return _default_timeout(req, **user_kw)
def _compose_data(req, **user_kw):
token, method, params, files = req
data = aiohttp.FormData()
if params:
for key,value in params.items():
data.add_field(key, str(value))
if files:
for key,f in files.items():
if isinstance(f, tuple):
if len(f) == 2:
filename, fileobj = f
else:
raise ValueError('Tuple must have exactly 2 elements: filename, fileobj')
else:
filename, fileobj = _guess_filename(f) or key, f
data.add_field(key, fileobj, filename=filename)
return data
def _transform(req, **user_kw):
timeout = _compose_timeout(req, **user_kw)
data = _compose_data(req, **user_kw)
url = _methodurl(req, **user_kw)
name = _which_pool(req, **user_kw)
if name is None:
session = _create_onetime_pool()
cleanup = session.close # one-time session: remember to close
else:
session = _pools[name]
cleanup = None # reuse: do not close
kwargs = {'data':data}
kwargs.update(user_kw)
return session.post, (url,), kwargs, timeout, cleanup
async def _parse(response):
try:
data = await response.json()
if data is None:
raise ValueError()
except (ValueError, json.JSONDecodeError, aiohttp.ClientResponseError):
text = await response.text()
raise exception.BadHTTPResponse(response.status, text, response)
if data['ok']:
return data['result']
else:
description, error_code = data['description'], data['error_code']
# Look for specific error ...
for e in exception.TelegramError.__subclasses__():
n = len(e.DESCRIPTION_PATTERNS)
if any(map(re.search, e.DESCRIPTION_PATTERNS, n*[description], n*[re.IGNORECASE])):
raise e(description, error_code, data)
# ... or raise generic error
raise exception.TelegramError(description, error_code, data)
async def request(req, **user_kw):
fn, args, kwargs, timeout, cleanup = _transform(req, **user_kw)
kwargs.update(_proxy_kwargs())
try:
if timeout is None:
async with fn(*args, **kwargs) as r:
return await _parse(r)
else:
try:
with async_timeout.timeout(timeout):
async with fn(*args, **kwargs) as r:
return await _parse(r)
except asyncio.TimeoutError:
raise exception.TelegramError('Response timeout', 504, {})
except aiohttp.ClientConnectionError:
raise exception.TelegramError('Connection Error', 400, {})
finally:
if cleanup: # e.g. closing one-time session
if asyncio.iscoroutinefunction(cleanup):
await cleanup()
else:
cleanup()
def download(req):
session = _create_onetime_pool()
kwargs = {}
kwargs.update(_proxy_kwargs())
return session, session.get(_fileurl(req), timeout=_timeout, **kwargs)
# Caller should close session after download is complete

View File

@@ -0,0 +1,106 @@
"""
Like :mod:`telepot.delegate`, this module has a bunch of seeder factories
and delegator factories.
.. autofunction:: per_chat_id
.. autofunction:: per_chat_id_in
.. autofunction:: per_chat_id_except
.. autofunction:: per_from_id
.. autofunction:: per_from_id_in
.. autofunction:: per_from_id_except
.. autofunction:: per_inline_from_id
.. autofunction:: per_inline_from_id_in
.. autofunction:: per_inline_from_id_except
.. autofunction:: per_application
.. autofunction:: per_message
.. autofunction:: per_event_source_id
.. autofunction:: per_callback_query_chat_id
.. autofunction:: per_callback_query_origin
.. autofunction:: per_invoice_payload
.. autofunction:: until
.. autofunction:: chain
.. autofunction:: pair
.. autofunction:: pave_event_space
.. autofunction:: include_callback_query_chat_id
.. autofunction:: intercept_callback_query_origin
"""
import asyncio
import traceback
from .. import exception
from . import helper
# Mirror traditional version to avoid having to import one more module
from ..delegate import (
per_chat_id, per_chat_id_in, per_chat_id_except,
per_from_id, per_from_id_in, per_from_id_except,
per_inline_from_id, per_inline_from_id_in, per_inline_from_id_except,
per_application, per_message, per_event_source_id,
per_callback_query_chat_id, per_callback_query_origin, per_invoice_payload,
until, chain, pair, pave_event_space,
include_callback_query_chat_id, intercept_callback_query_origin
)
def _ensure_coroutine_function(fn):
return fn if asyncio.iscoroutinefunction(fn) else asyncio.coroutine(fn)
def call(corofunc, *args, **kwargs):
"""
:return:
a delegator function that returns a coroutine object by calling
``corofunc(seed_tuple, *args, **kwargs)``.
"""
corofunc = _ensure_coroutine_function(corofunc)
def f(seed_tuple):
return corofunc(seed_tuple, *args, **kwargs)
return f
def create_run(cls, *args, **kwargs):
"""
:return:
a delegator function that calls the ``cls`` constructor whose arguments being
a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns
a coroutine object by calling the object's ``run`` method, which should be
a coroutine function.
"""
def f(seed_tuple):
j = cls(seed_tuple, *args, **kwargs)
return _ensure_coroutine_function(j.run)()
return f
def create_open(cls, *args, **kwargs):
"""
:return:
a delegator function that calls the ``cls`` constructor whose arguments being
a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns
a looping coroutine object that uses the object's ``listener`` to wait for
messages and invokes instance method ``open``, ``on_message``, and ``on_close``
accordingly.
"""
def f(seed_tuple):
j = cls(seed_tuple, *args, **kwargs)
async def wait_loop():
bot, msg, seed = seed_tuple
try:
handled = await helper._invoke(j.open, msg, seed)
if not handled:
await helper._invoke(j.on_message, msg)
while 1:
msg = await j.listener.wait()
await helper._invoke(j.on_message, msg)
# These exceptions are "normal" exits.
except (exception.IdleTerminate, exception.StopListening) as e:
await helper._invoke(j.on_close, e)
# Any other exceptions are accidents. **Print it out.**
# This is to prevent swallowing exceptions in the case that on_close()
# gets overridden but fails to account for unexpected exceptions.
except Exception as e:
traceback.print_exc()
await helper._invoke(j.on_close, e)
return wait_loop()
return f

View File

@@ -0,0 +1,36 @@
try:
import aiohttp
from urllib.parse import quote
def content_disposition_header(disptype, quote_fields=True, **params):
if not disptype or not (aiohttp.helpers.TOKEN > set(disptype)):
raise ValueError('bad content disposition type {!r}'
''.format(disptype))
value = disptype
if params:
lparams = []
for key, val in params.items():
if not key or not (aiohttp.helpers.TOKEN > set(key)):
raise ValueError('bad content disposition parameter'
' {!r}={!r}'.format(key, val))
###### Do not encode filename
if key == 'filename':
qval = val
else:
qval = quote(val, '') if quote_fields else val
lparams.append((key, '"%s"' % qval))
sparams = '; '.join('='.join(pair) for pair in lparams)
value = '; '.join((value, sparams))
return value
# Override original version
aiohttp.payload.content_disposition_header = content_disposition_header
# In case aiohttp changes and this hack no longer works, I don't want it to
# bog down the entire library.
except (ImportError, AttributeError):
pass

View File

@@ -0,0 +1,372 @@
import asyncio
import traceback
from .. import filtering, helper, exception
from .. import (
flavor, chat_flavors, inline_flavors, is_event,
message_identifier, origin_identifier)
# Mirror traditional version
from ..helper import (
Sender, Administrator, Editor, openable,
StandardEventScheduler, StandardEventMixin)
async def _invoke(fn, *args, **kwargs):
if asyncio.iscoroutinefunction(fn):
return await fn(*args, **kwargs)
else:
return fn(*args, **kwargs)
def _create_invoker(obj, method_name):
async def d(*a, **kw):
method = getattr(obj, method_name)
return await _invoke(method, *a, **kw)
return d
class Microphone(object):
def __init__(self):
self._queues = set()
def add(self, q):
self._queues.add(q)
def remove(self, q):
self._queues.remove(q)
def send(self, msg):
for q in self._queues:
try:
q.put_nowait(msg)
except asyncio.QueueFull:
traceback.print_exc()
pass
class Listener(helper.Listener):
async def wait(self):
"""
Block until a matched message appears.
"""
if not self._patterns:
raise RuntimeError('Listener has nothing to capture')
while 1:
msg = await self._queue.get()
if any(map(lambda p: filtering.match_all(msg, p), self._patterns)):
return msg
from concurrent.futures._base import CancelledError
class Answerer(object):
"""
When processing inline queries, ensures **at most one active task** per user id.
"""
def __init__(self, bot, loop=None):
self._bot = bot
self._loop = loop if loop is not None else asyncio.get_event_loop()
self._working_tasks = {}
def answer(self, inline_query, compute_fn, *compute_args, **compute_kwargs):
"""
Create a task that calls ``compute fn`` (along with additional arguments
``*compute_args`` and ``**compute_kwargs``), then applies the returned value to
:meth:`.Bot.answerInlineQuery` to answer the inline query.
If a preceding task is already working for a user, that task is cancelled,
thus ensuring at most one active task per user id.
:param inline_query:
The inline query to be processed. The originating user is inferred from ``msg['from']['id']``.
:param compute_fn:
A function whose returned value is given to :meth:`.Bot.answerInlineQuery` to send.
May return:
- a *list* of `InlineQueryResult <https://core.telegram.org/bots/api#inlinequeryresult>`_
- a *tuple* whose first element is a list of `InlineQueryResult <https://core.telegram.org/bots/api#inlinequeryresult>`_,
followed by positional arguments to be supplied to :meth:`.Bot.answerInlineQuery`
- a *dictionary* representing keyword arguments to be supplied to :meth:`.Bot.answerInlineQuery`
:param \*compute_args: positional arguments to ``compute_fn``
:param \*\*compute_kwargs: keyword arguments to ``compute_fn``
"""
from_id = inline_query['from']['id']
async def compute_and_answer():
try:
query_id = inline_query['id']
ans = await _invoke(compute_fn, *compute_args, **compute_kwargs)
if isinstance(ans, list):
await self._bot.answerInlineQuery(query_id, ans)
elif isinstance(ans, tuple):
await self._bot.answerInlineQuery(query_id, *ans)
elif isinstance(ans, dict):
await self._bot.answerInlineQuery(query_id, **ans)
else:
raise ValueError('Invalid answer format')
except CancelledError:
# Cancelled. Record has been occupied by new task. Don't touch.
raise
except:
# Die accidentally. Remove myself from record.
del self._working_tasks[from_id]
raise
else:
# Die naturally. Remove myself from record.
del self._working_tasks[from_id]
if from_id in self._working_tasks:
self._working_tasks[from_id].cancel()
t = self._loop.create_task(compute_and_answer())
self._working_tasks[from_id] = t
class AnswererMixin(helper.AnswererMixin):
Answerer = Answerer # use async Answerer class
class CallbackQueryCoordinator(helper.CallbackQueryCoordinator):
def augment_send(self, send_func):
async def augmented(*aa, **kw):
sent = await send_func(*aa, **kw)
if self._enable_chat and self._contains_callback_data(kw):
self.capture_origin(message_identifier(sent))
return sent
return augmented
def augment_edit(self, edit_func):
async def augmented(msg_identifier, *aa, **kw):
edited = await edit_func(msg_identifier, *aa, **kw)
if (edited is True and self._enable_inline) or (isinstance(edited, dict) and self._enable_chat):
if self._contains_callback_data(kw):
self.capture_origin(msg_identifier)
else:
self.uncapture_origin(msg_identifier)
return edited
return augmented
def augment_delete(self, delete_func):
async def augmented(msg_identifier, *aa, **kw):
deleted = await delete_func(msg_identifier, *aa, **kw)
if deleted is True:
self.uncapture_origin(msg_identifier)
return deleted
return augmented
def augment_on_message(self, handler):
async def augmented(msg):
if (self._enable_inline
and flavor(msg) == 'chosen_inline_result'
and 'inline_message_id' in msg):
inline_message_id = msg['inline_message_id']
self.capture_origin(inline_message_id)
return await _invoke(handler, msg)
return augmented
class InterceptCallbackQueryMixin(helper.InterceptCallbackQueryMixin):
CallbackQueryCoordinator = CallbackQueryCoordinator
class IdleEventCoordinator(helper.IdleEventCoordinator):
def augment_on_message(self, handler):
async def augmented(msg):
# Reset timer if this is an external message
is_event(msg) or self.refresh()
return await _invoke(handler, msg)
return augmented
def augment_on_close(self, handler):
async def augmented(ex):
try:
if self._timeout_event:
self._scheduler.cancel(self._timeout_event)
self._timeout_event = None
# This closing may have been caused by my own timeout, in which case
# the timeout event can no longer be found in the scheduler.
except exception.EventNotFound:
self._timeout_event = None
return await _invoke(handler, ex)
return augmented
class IdleTerminateMixin(helper.IdleTerminateMixin):
IdleEventCoordinator = IdleEventCoordinator
class Router(helper.Router):
async def route(self, msg, *aa, **kw):
"""
Apply key function to ``msg`` to obtain a key, look up routing table
to obtain a handler function, then call the handler function with
positional and keyword arguments, if any is returned by the key function.
``*aa`` and ``**kw`` are dummy placeholders for easy nesting.
Regardless of any number of arguments returned by the key function,
multi-level routing may be achieved like this::
top_router.routing_table['key1'] = sub_router1.route
top_router.routing_table['key2'] = sub_router2.route
"""
k = self.key_function(msg)
if isinstance(k, (tuple, list)):
key, args, kwargs = {1: tuple(k) + ((),{}),
2: tuple(k) + ({},),
3: tuple(k),}[len(k)]
else:
key, args, kwargs = k, (), {}
try:
fn = self.routing_table[key]
except KeyError as e:
# Check for default handler, key=None
if None in self.routing_table:
fn = self.routing_table[None]
else:
raise RuntimeError('No handler for key: %s, and default handler not defined' % str(e.args))
return await _invoke(fn, msg, *args, **kwargs)
class DefaultRouterMixin(object):
def __init__(self, *args, **kwargs):
self._router = Router(flavor, {'chat': _create_invoker(self, 'on_chat_message'),
'callback_query': _create_invoker(self, 'on_callback_query'),
'inline_query': _create_invoker(self, 'on_inline_query'),
'chosen_inline_result': _create_invoker(self, 'on_chosen_inline_result'),
'shipping_query': _create_invoker(self, 'on_shipping_query'),
'pre_checkout_query': _create_invoker(self, 'on_pre_checkout_query'),
'_idle': _create_invoker(self, 'on__idle')})
super(DefaultRouterMixin, self).__init__(*args, **kwargs)
@property
def router(self):
""" See :class:`.helper.Router` """
return self._router
async def on_message(self, msg):
"""
Called when a message is received.
By default, call :meth:`Router.route` to handle the message.
"""
await self._router.route(msg)
@openable
class Monitor(helper.ListenerContext, DefaultRouterMixin):
def __init__(self, seed_tuple, capture, **kwargs):
"""
A delegate that never times-out, probably doing some kind of background monitoring
in the application. Most naturally paired with :func:`telepot.aio.delegate.per_application`.
:param capture: a list of patterns for ``listener`` to capture
"""
bot, initial_msg, seed = seed_tuple
super(Monitor, self).__init__(bot, seed, **kwargs)
for pattern in capture:
self.listener.capture(pattern)
@openable
class ChatHandler(helper.ChatContext,
DefaultRouterMixin,
StandardEventMixin,
IdleTerminateMixin):
def __init__(self, seed_tuple,
include_callback_query=False, **kwargs):
"""
A delegate to handle a chat.
"""
bot, initial_msg, seed = seed_tuple
super(ChatHandler, self).__init__(bot, seed, **kwargs)
self.listener.capture([{'chat': {'id': self.chat_id}}])
if include_callback_query:
self.listener.capture([{'message': {'chat': {'id': self.chat_id}}}])
@openable
class UserHandler(helper.UserContext,
DefaultRouterMixin,
StandardEventMixin,
IdleTerminateMixin):
def __init__(self, seed_tuple,
include_callback_query=False,
flavors=chat_flavors+inline_flavors, **kwargs):
"""
A delegate to handle a user's actions.
:param flavors:
A list of flavors to capture. ``all`` covers all flavors.
"""
bot, initial_msg, seed = seed_tuple
super(UserHandler, self).__init__(bot, seed, **kwargs)
if flavors == 'all':
self.listener.capture([{'from': {'id': self.user_id}}])
else:
self.listener.capture([lambda msg: flavor(msg) in flavors, {'from': {'id': self.user_id}}])
if include_callback_query:
self.listener.capture([{'message': {'chat': {'id': self.user_id}}}])
class InlineUserHandler(UserHandler):
def __init__(self, seed_tuple, **kwargs):
"""
A delegate to handle a user's inline-related actions.
"""
super(InlineUserHandler, self).__init__(seed_tuple, flavors=inline_flavors, **kwargs)
@openable
class CallbackQueryOriginHandler(helper.CallbackQueryOriginContext,
DefaultRouterMixin,
StandardEventMixin,
IdleTerminateMixin):
def __init__(self, seed_tuple, **kwargs):
"""
A delegate to handle callback query from one origin.
"""
bot, initial_msg, seed = seed_tuple
super(CallbackQueryOriginHandler, self).__init__(bot, seed, **kwargs)
self.listener.capture([
lambda msg:
flavor(msg) == 'callback_query' and origin_identifier(msg) == self.origin
])
@openable
class InvoiceHandler(helper.InvoiceContext,
DefaultRouterMixin,
StandardEventMixin,
IdleTerminateMixin):
def __init__(self, seed_tuple, **kwargs):
"""
A delegate to handle messages related to an invoice.
"""
bot, initial_msg, seed = seed_tuple
super(InvoiceHandler, self).__init__(bot, seed, **kwargs)
self.listener.capture([{'invoice_payload': self.payload}])
self.listener.capture([{'successful_payment': {'invoice_payload': self.payload}}])

View File

@@ -0,0 +1,205 @@
import asyncio
import time
import traceback
import collections
from concurrent.futures._base import CancelledError
from . import flavor_router
from ..loop import _extract_message, _dictify
from .. import exception
class GetUpdatesLoop(object):
def __init__(self, bot, on_update):
self._bot = bot
self._update_handler = on_update
async def run_forever(self, relax=0.1, offset=None, timeout=20, allowed_updates=None):
"""
Process new updates in infinity loop
:param relax: float
:param offset: int
:param timeout: int
:param allowed_updates: bool
"""
while 1:
try:
result = await self._bot.getUpdates(offset=offset,
timeout=timeout,
allowed_updates=allowed_updates)
# Once passed, this parameter is no longer needed.
allowed_updates = None
# No sort. Trust server to give messages in correct order.
for update in result:
self._update_handler(update)
offset = update['update_id'] + 1
except CancelledError:
break
except exception.BadHTTPResponse as e:
traceback.print_exc()
# Servers probably down. Wait longer.
if e.status == 502:
await asyncio.sleep(30)
except:
traceback.print_exc()
await asyncio.sleep(relax)
else:
await asyncio.sleep(relax)
def _infer_handler_function(bot, h):
if h is None:
handler = bot.handle
elif isinstance(h, dict):
handler = flavor_router(h)
else:
handler = h
def create_task_for(msg):
bot.loop.create_task(handler(msg))
if asyncio.iscoroutinefunction(handler):
return create_task_for
else:
return handler
class MessageLoop(object):
def __init__(self, bot, handle=None):
self._bot = bot
self._handle = _infer_handler_function(bot, handle)
self._task = None
async def run_forever(self, *args, **kwargs):
updatesloop = GetUpdatesLoop(self._bot,
lambda update:
self._handle(_extract_message(update)[1]))
self._task = self._bot.loop.create_task(updatesloop.run_forever(*args, **kwargs))
self._bot.scheduler.on_event(self._handle)
def cancel(self):
self._task.cancel()
class Webhook(object):
def __init__(self, bot, handle=None):
self._bot = bot
self._handle = _infer_handler_function(bot, handle)
async def run_forever(self):
self._bot.scheduler.on_event(self._handle)
def feed(self, data):
update = _dictify(data)
self._handle(_extract_message(update)[1])
class OrderedWebhook(object):
def __init__(self, bot, handle=None):
self._bot = bot
self._handle = _infer_handler_function(bot, handle)
self._update_queue = asyncio.Queue(loop=bot.loop)
async def run_forever(self, maxhold=3):
self._bot.scheduler.on_event(self._handle)
def extract_handle(update):
try:
self._handle(_extract_message(update)[1])
except:
# Localize the error so message thread can keep going.
traceback.print_exc()
finally:
return update['update_id']
# Here is the re-ordering mechanism, ensuring in-order delivery of updates.
max_id = None # max update_id passed to callback
buffer = collections.deque() # keep those updates which skip some update_id
qwait = None # how long to wait for updates,
# because buffer's content has to be returned in time.
while 1:
try:
update = await asyncio.wait_for(self._update_queue.get(), qwait)
if max_id is None:
# First message received, handle regardless.
max_id = extract_handle(update)
elif update['update_id'] == max_id + 1:
# No update_id skipped, handle naturally.
max_id = extract_handle(update)
# clear contagious updates in buffer
if len(buffer) > 0:
buffer.popleft() # first element belongs to update just received, useless now.
while 1:
try:
if type(buffer[0]) is dict:
max_id = extract_handle(buffer.popleft()) # updates that arrived earlier, handle them.
else:
break # gap, no more contagious updates
except IndexError:
break # buffer empty
elif update['update_id'] > max_id + 1:
# Update arrives pre-maturely, insert to buffer.
nbuf = len(buffer)
if update['update_id'] <= max_id + nbuf:
# buffer long enough, put update at position
buffer[update['update_id'] - max_id - 1] = update
else:
# buffer too short, lengthen it
expire = time.time() + maxhold
for a in range(nbuf, update['update_id']-max_id-1):
buffer.append(expire) # put expiry time in gaps
buffer.append(update)
else:
pass # discard
except asyncio.TimeoutError:
# debug message
# print('Timeout')
# some buffer contents have to be handled
# flush buffer until a non-expired time is encountered
while 1:
try:
if type(buffer[0]) is dict:
max_id = extract_handle(buffer.popleft())
else:
expire = buffer[0]
if expire <= time.time():
max_id += 1
buffer.popleft()
else:
break # non-expired
except IndexError:
break # buffer empty
except:
traceback.print_exc()
finally:
try:
# don't wait longer than next expiry time
qwait = buffer[0] - time.time()
if qwait < 0:
qwait = 0
except IndexError:
# buffer empty, can wait forever
qwait = None
# debug message
# print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id)
def feed(self, data):
update = _dictify(data)
self._update_queue.put_nowait(update)

View File

@@ -0,0 +1,46 @@
from .helper import _create_invoker
from .. import all_content_types
# Mirror traditional version to avoid having to import one more module
from ..routing import (
by_content_type, by_command, by_chat_command, by_text, by_data, by_regex,
process_key, lower_key, upper_key
)
def make_routing_table(obj, keys, prefix='on_'):
"""
:return:
a dictionary roughly equivalent to ``{'key1': obj.on_key1, 'key2': obj.on_key2, ...}``,
but ``obj`` does not have to define all methods. It may define the needed ones only.
:param obj: the object
:param keys: a list of keys
:param prefix: a string to be prepended to keys to make method names
"""
def maptuple(k):
if isinstance(k, tuple):
if len(k) == 2:
return k
elif len(k) == 1:
return k[0], _create_invoker(obj, prefix+k[0])
else:
raise ValueError()
else:
return k, _create_invoker(obj, prefix+k)
return dict([maptuple(k) for k in keys])
def make_content_type_routing_table(obj, prefix='on_'):
"""
:return:
a dictionary covering all available content types, roughly equivalent to
``{'text': obj.on_text, 'photo': obj.on_photo, ...}``,
but ``obj`` does not have to define all methods. It may define the needed ones only.
:param obj: the object
:param prefix: a string to be prepended to content types to make method names
"""
return make_routing_table(obj, all_content_types, prefix)