update
This commit is contained in:
926
lib/support/telepot2/aio/__init__.py
Normal file
926
lib/support/telepot2/aio/__init__.py
Normal 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)
|
||||
168
lib/support/telepot2/aio/api.py
Normal file
168
lib/support/telepot2/aio/api.py
Normal 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
|
||||
106
lib/support/telepot2/aio/delegate.py
Normal file
106
lib/support/telepot2/aio/delegate.py
Normal 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
|
||||
36
lib/support/telepot2/aio/hack.py
Normal file
36
lib/support/telepot2/aio/hack.py
Normal 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
|
||||
372
lib/support/telepot2/aio/helper.py
Normal file
372
lib/support/telepot2/aio/helper.py
Normal 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}}])
|
||||
205
lib/support/telepot2/aio/loop.py
Normal file
205
lib/support/telepot2/aio/loop.py
Normal 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)
|
||||
46
lib/support/telepot2/aio/routing.py
Normal file
46
lib/support/telepot2/aio/routing.py
Normal 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)
|
||||
Reference in New Issue
Block a user