diff --git a/telepot/namedtuple.py b/telepot/namedtuple.py index a5c570b..7d7fd1b 100644 --- a/telepot/namedtuple.py +++ b/telepot/namedtuple.py @@ -110,7 +110,7 @@ def UserArray(data): # incoming ChatPhoto = _create_class('ChatPhoto', [ 'small_file_id', - 'big_file_id', + 'big_file_id' ]) # incoming @@ -127,7 +127,7 @@ def UserArray(data): 'invite_link', _Field('pinned_message', constructor=_Message), 'sticker_set_name', - 'can_set_sticker_set', + 'can_set_sticker_set' ]) # incoming @@ -136,7 +136,7 @@ def UserArray(data): 'width', 'height', 'file_size', - 'file_path', # undocumented + 'file_path' # undocumented ]) # incoming @@ -146,7 +146,8 @@ def UserArray(data): 'performer', 'title', 'mime_type', - 'file_size' + 'file_size', + _Field('thumb', constructor=PhotoSize) ]) # incoming @@ -156,7 +157,7 @@ def UserArray(data): 'file_name', 'mime_type', 'file_size', - 'file_path', # undocumented + 'file_path' # undocumented ]) # incoming and outgoing @@ -164,7 +165,7 @@ def UserArray(data): 'point', 'x_shift', 'y_shift', - 'scale', + 'scale' ]) # incoming @@ -176,7 +177,7 @@ def UserArray(data): 'emoji', 'set_name', _Field('mask_position', constructor=MaskPosition), - 'file_size', + 'file_size' ]) def StickerArray(data): @@ -187,7 +188,7 @@ def StickerArray(data): 'name', 'title', 'contains_masks', - _Field('stickers', constructor=StickerArray), + _Field('stickers', constructor=StickerArray) ]) # incoming @@ -280,6 +281,7 @@ def PhotoSizeArrayArray(data): 'can_add_web_page_previews', ]) + def ChatMemberArray(data): return [ChatMember(**p) for p in data] @@ -432,7 +434,7 @@ def MessageEntityArray(data): 'shipping_option_id', _Field('order_info', constructor=OrderInfo), 'telegram_payment_charge_id', - 'provider_payment_charge_id', + 'provider_payment_charge_id' ]) # incoming @@ -479,6 +481,7 @@ def MessageEntityArray(data): _Field('invoice', constructor=Invoice), _Field('successful_payment', constructor=SuccessfulPayment), 'connected_website', + _Field('animation', constructor=Animation) ]) # incoming @@ -487,7 +490,7 @@ def MessageEntityArray(data): _Field('from_', constructor=User), _Field('location', constructor=Location), 'query', - 'offset', + 'offset' ]) # incoming @@ -496,7 +499,7 @@ def MessageEntityArray(data): _Field('from_', constructor=User), _Field('location', constructor=Location), 'inline_message_id', - 'query', + 'query' ]) # incoming @@ -507,7 +510,7 @@ def MessageEntityArray(data): 'inline_message_id', 'chat_instance', 'data', - 'game_short_name', + 'game_short_name' ]) # incoming @@ -519,7 +522,7 @@ def MessageEntityArray(data): _Field('edited_channel_post', constructor=Message), _Field('inline_query', constructor=InlineQuery), _Field('chosen_inline_result', constructor=ChosenInlineResult), - _Field('callback_query', constructor=CallbackQuery), + _Field('callback_query', constructor=CallbackQuery) ]) # incoming diff --git a/update_desc/README.md b/update_desc/README.md new file mode 100644 index 0000000..4e4ac6d --- /dev/null +++ b/update_desc/README.md @@ -0,0 +1,2 @@ +This collection of directories contain READMEs which describe the process to get the project up to the point where is is compatible with the Telegram Bot API as described in https://core.telegram.org/bots/api . + diff --git a/update_desc/TG_bot_API_4.0/README.md b/update_desc/TG_bot_API_4.0/README.md new file mode 100644 index 0000000..89eb47e --- /dev/null +++ b/update_desc/TG_bot_API_4.0/README.md @@ -0,0 +1,2 @@ +This collection of READMEs describe the process to get the project up to the point where is is compatible with the Telegram Bot API as described in https://core.telegram.org/bots/api#july-26-2018 . + diff --git a/update_desc/TG_bot_API_4.0/README__20180808_1016.txt b/update_desc/TG_bot_API_4.0/README__20180808_1016.txt new file mode 100644 index 0000000..037763e --- /dev/null +++ b/update_desc/TG_bot_API_4.0/README__20180808_1016.txt @@ -0,0 +1,115 @@ +Let me brief on anyone who wants to make changes to accommodate the latest Bot API (4.0). I start with more trivial ones, then move on to harder ones. + +Telegram says: + + Added the field thumb to the Audio object to contain the thumbnail of the album cover to which the + music file belongs. + +Key phrase is "Added the field thumb to the Audio object". In telepot, the habit is to represent API objects using Python dictionaries, so a new field in an object should not affect us. However, to facilitate accessing a field using . notation, there is a way to convert a dictionary into a namedtuple. As a result, every API object has a corresponding namedtuple. When a field is added to an object, we need to make a corresponding change to that namedtuple. The file is namedtuple.py. The original definition of Audio is: + +# incoming +Audio = _create_class('Audio', [ + 'file_id', + 'duration', + 'performer', + 'title', + 'mime_type', + 'file_size' + ]) + +With the addition of a thumb field of the type PhotoSize, it should be modified as: + +# incoming +Audio = _create_class('Audio', [ + 'file_id', + 'duration', + 'performer', + 'title', + 'mime_type', + 'file_size', + _Field('thumb', constructor=PhotoSize), + ]) + +The thumb field looks more complicated than others because its type is not primitive (int, float, string, etc) and we +need to tell it to interpret the dictionary in that place into another namedtuple. The "constructor" in this case is +just the name of the target namedtuple, and can be considered a parsing hint. + +Also note the comment above the namedtuple definition. They can be: + + incoming: meaning we only receive the object from Telegram servers, but never send it out + outgoing: meaning we only send it out, but never receive it + or both (rarely) + +This distinction is important because: + + for incoming namedtuples, we must make sure all non-primitive fields be given a "constructor" like above. Otherwise, that field would remain a dictionary, which defeats the purpose of using namedtuples. This is not needed for outgoing namedtuples because fields are supplied by user, so no parsing hint is required, + + for outgoing namedtuples, some fields have default values. For example, InlineQueryResultArticle's type field is default to article, as required by Bot API. In contrast, no field of any incoming namedtuples has default values. + +DONE: 20180811_1742 (UH) + +######################################################################################################################## + +Telegram says: + + Added the field animation to the Message object. For backward compatibility, when this field is set, the document + field will be also set. + +Key phrase: Added the field animation to the Message object. Points to consider: + + Message is an incoming namedtuple + the field animation is of the type Animation + +Changes should be similar. I leave that to the reader as an exercise. + +DONE: 20180811_1752 (UH) + +######################################################################################################################## + +Telegram says: + + Added support for Foursquare venues: added the new field foursquare_type to the objects + Venue, InlineQueryResultVenue and InputVenueMessageContent, and the parameter foursquare_type + to the sendVenue method. + +Points to consider: + + The field foursquare_type is of the type String. No parsing hint (if incoming) is needed. + Venue is incoming + InlineQueryResultVenue and InputVenueMessageContent are outgoing + +I also leave the changes as an exercise. + +As for the method sendVenue, I will delay the discussion until later, lumping it together with other method changes. + +DONE: 201808 (UH) + +######################################################################################################################## + +Telegram says: + + Added vCard support when sharing contacts: added the field vcard to the objects Contact, InlineQueryResultContact, InputContactMessageContent and the method sendContact. + + The field vcard is of the type String. + Contact is incoming + InlineQueryResultContact and InputContactMessageContent are outgoing + +I will delay the discussion of the method sendContact similarly. + +DONE: 201808 (UH) + +######################################################################################################################## + +Telegram says: + + Added support for editing the media content of messages: added the method editMessageMedia and new types InputMediaAnimation, InputMediaAudio, and InputMediaDocument. + +Finally, we have to create new types of namedtuple here. + + InputMediaAnimation, InputMediaAudio, and InputMediaDocument are all outgoing + +Luckily, there are two InputMedia* siblings in existence already: InputMediaPhoto and InputMediaVideo. Find them, and use them as templates for the new members. + +DONE: 201808 (UH) + +######################################################################################################################## diff --git a/update_desc/TG_bot_API_4.0/README__20180809_1442.txt b/update_desc/TG_bot_API_4.0/README__20180809_1442.txt new file mode 100644 index 0000000..3c43337 --- /dev/null +++ b/update_desc/TG_bot_API_4.0/README__20180809_1442.txt @@ -0,0 +1,99 @@ +Tonight, let's see how to change methods. + +Telegram says: + + Added support for Foursquare venues: added ... the parameter foursquare_type to the sendVenue method. + + Added vCard support when sharing contacts: added the field vcard to ... the method sendContact. + +The two methods, sendVenue and sendContact, are adjacent in the file __init__.py, within the Bot class. Let's look at them together. Here are the original: + +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 self._api_request('sendVenue', _rectify(p)) + +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 self._api_request('sendContact', _rectify(p)) + +Changes are straight-forward: + +def sendVenue(self, chat_id, latitude, longitude, title, address, + foursquare_id=None, + foursquare_type=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ See: https://core.telegram.org/bots/api#sendvenue """ + p = _strip(locals()) + return self._api_request('sendVenue', _rectify(p)) + +def sendContact(self, chat_id, phone_number, first_name, + last_name=None, + vcard=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ See: https://core.telegram.org/bots/api#sendcontact """ + p = _strip(locals()) + return self._api_request('sendContact', _rectify(p)) + +Both are optional parameters, so default to None. There's nothing to change in the method body. _strip(locals()) puts all method parameters other than self into a dict. _rectify(p) removes all None values before passing them to self._api_request(). + +Telegram says: + + Added the method sendAnimation, which can be used instead of sendDocument to send animations, specifying their duration, width and height. + +Luckily, we have a lot of send* methods to copy from, e.g. sendDocument, sendVideo, sendVoice, etc. I use sendVideo as an example: + +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.Bot.sendPhoto` + """ + p = _strip(locals(), more=['video']) + return self._api_request_with_file('sendVideo', _rectify(p), 'video', video) + +Making sendAnimation is just a matter of fixing names and matching method parameters with Telegram docs: + +def sendAnimation(self, chat_id, animation, + duration=None, + width=None, + height=None, + caption=None, + parse_mode=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#sendanimation + + :param video: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` + """ + p = _strip(locals(), more=['animation']) + return self._api_request_with_file('sendAnimation', _rectify(p), 'animation', animation) + +Sending files takes some special handling. That's why I have to _strip the animation and pass it to self._api_request_with_file() outside of the rectified dict. + +Unfortunately, I wasn't aware of the "thumb" parameter, which must have been added at some earlier date. To take care of it, we have to modify _api_request_with_file() to handle one more file to be uploaded. + diff --git a/update_desc/TG_bot_API_4.0/README__20180810_1532.txt b/update_desc/TG_bot_API_4.0/README__20180810_1532.txt new file mode 100644 index 0000000..ce7a748 --- /dev/null +++ b/update_desc/TG_bot_API_4.0/README__20180810_1532.txt @@ -0,0 +1,113 @@ +Last night was the first time I am aware of the thumb parameter. Its addition requires me to cram one more file to the call to _api_request_with_file(), whose original version is this: + +def _api_request_with_file(self, method, params, file_key, file_value, **kwargs): + if _isstring(file_value): + params[file_key] = file_value + return self._api_request(method, _rectify(params), **kwargs) + else: + files = {file_key: file_value} + return self._api_request(method, _rectify(params), files, **kwargs) + +Bot API allows file_value to be either a string (serving as a file id which refers to an existing file on Telegram servers) or a local file to be uploaded. That's why I have to distinguish between file_value being a string or not above. If it is a string, merge it to params. If not a string, I assume it's a file and pass it separately. Note that variable files is a dict. + +Now, I want _api_request_with_file() to be able to handle multiple files. I would squeeze file_key and file_value into one parameter of dict: + +def _api_request_with_file(self, method, params, files, **kwargs): + params.update({ + k:v for k,v in files.items() if _isstring(v) }) + + files = { + k:v for k,v in files.items() if v is not None and not _isstring(v) } + + return self._api_request(method, _rectify(params), files, **kwargs) + +I hate sprinkling trivial comments among code, so I explain here: + + params.update({ k:v for k,v in files.items() if _isstring(v) }) basically extracts all string values from files and merges them with params + files = { k:v for k,v in files.items() if v is not None and not _isstring(v) } basically extracts all non-null non-string values from files and reassigns them to the variable files + +With this change to _api_request_with_file() and the addition of the thumb parameter, sendAnimation should looks like this: + +def sendAnimation(self, chat_id, animation, + duration=None, + width=None, + height=None, + thumb=None, + caption=None, + parse_mode=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#sendanimation + + :param video: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` + """ + p = _strip(locals(), more=['animation', 'thumb']) + return self._api_request_with_file('sendAnimation', + _rectify(p), + {'animation': animation, 'thumb': thumb}) + +If you don't mind being a bit venturesome, let me suggest one more change regarding the function _strip(). Instead of simply removing certain parameters from locals(), I want it to package them into a separate dict, so I can pass it directly. + +Originally: + +def _strip(params, more=[]): + return {key: value for key,value in params.items() if key not in ['self']+more} + +I would change it to: + +def _strip(params, files=[]): + return ( + { k:v for k,v in params.items() if k not in ['self']+files }, + { k:v for k,v in params.items() if k in files }) + +Then, sendAnimation becomes: + +def sendAnimation(self, chat_id, animation, + duration=None, + width=None, + height=None, + thumb=None, + caption=None, + parse_mode=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#sendanimation + + :param video: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` + """ + p,f = _strip(locals(), files=['animation', 'thumb']) + return self._api_request_with_file('sendAnimation', _rectify(p), _rectify(f)) + +Remember to add thumb parameter to all other relevant methods. For example, sendVideo should look like: + +def sendVideo(self, chat_id, video, + duration=None, + width=None, + height=None, + thumb=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.Bot.sendPhoto` + """ + p,f = _strip(locals(), files=['video', 'thumb']) + return self._api_request_with_file('sendVideo', _rectify(p), _rectify(f)) + +Because of the change to _strip's return value, all calls to it should be re-examined and changed to: + +p,f = _strip(locals()) + +Please also note that none of the above has been tested, although I am confident that they should not be far off. + +There are more changes to be made. Let's continue some time later. Good luck. + diff --git a/update_desc/TG_bot_API_4.0/README__20180811_1109.txt b/update_desc/TG_bot_API_4.0/README__20180811_1109.txt new file mode 100644 index 0000000..3f42218 --- /dev/null +++ b/update_desc/TG_bot_API_4.0/README__20180811_1109.txt @@ -0,0 +1,68 @@ + + +After one night of sleeping, I realize some bugs and shortcomings in last night's changes. I am going to fix them today. + +The function _rectify(dict) serves two purposes: + + If any values is a list, dict, or tuple, _rectify flattens it into a JSON-encoded string. + + It filters out null values. + +I always use _rectify to clean/normalize a dict before passing it to _api_request_with_file(). However, in last night's changes, I made the mistake of doing _rectify(f), using _rectify to clean the files dict. + +The values in f normally are either strings (file id on Telegram servers) or file objects (local files to be uploaded). But they could also be tuples, to include the filename in addition to the file object. When _rectify sees a tuple, it flattens it into a JSON-encoded string, which is wrong in this case. I only want it to filter out nulls. + +Those changes I proposed yesterday now become this: + +def _split(params, files=[]): + return ( + { k:v for k,v in params.items() if k not in ['self']+files }, + { k:v for k,v in params.items() if k in files }) + +def _nonull(params): + return { k:v for k,v in params.items() if v is not None } + +def _rectify(params): + # + # no change + # + + +class Bot(_BotBase): + def _api_request(self, method, params=None, files=None, **kwargs): + # + # no change + # + + def _api_request_with_file(self, method, params, files, **kwargs): + params.update({ + k:v for k,v in files.items() if _isstring(v) }) + + files = { + k:v for k,v in files.items() if not _isstring(v) } + + return self._api_request(method, params, files, **kwargs) + + + def sendMessage( ... ): + p,f = _split(locals()) + return self._api_request('sendMessage', _rectify(p)) + + def sendAnimation( ... ): + p,f = _split(locals(), files=['animation', 'thumb']) + return self._api_request_with_file('sendAnimation', _rectify(p), _nonull(f)) + + _strip is renamed to _split, meaning to split parameters into regular ones and file ones. + + Add a function _nonull() whose only job is to remove null values from a dict. + + _api_request_with_file() assumes the supplied dicts are always cleaned and normalized. No need to worry about null values inside. + + sendMessage() demonstrates how to implement a method with no file attached. + + sendAnimation() demonstrates how to implement a method with files attached potentially. + +With the repeated applications of _rectify() and _nonull(), you may prefer to hide them in one more level of function call, or even hide them in _api_request() and _api_request_with_file(). I think it's just a matter of taste and style. I prefer the transparency, to write and see them explicitly, to remind myself that parameters should be cleaned and normalized before use. + +Ok. That's it for today. We still haven't finished the job. I will continue some time later. +