-![GitHub All Releases](https://img.shields.io/github/downloads/noam09/deluge-telegramer/total?style=flat-square) ![Deluge Version](https://img.shields.io/badge/deluge-1.3.15-blue?style=flat-square&logo=deluge)
+![GitHub All Releases](https://img.shields.io/github/downloads/noam09/deluge-telegramer/total?style=flat-square) [![Deluge Version](https://img.shields.io/badge/deluge-1.3.15-blue?style=flat-square&logo=deluge)](https://github.com/noam09/deluge-telegramer/releases/tag/v1.3.1) [![Deluge Version](https://img.shields.io/badge/deluge-2.1.1-yellowgreen?style=flat-square&logo=deluge)](https://github.com/noam09/deluge-telegramer/releases/tag/2.1.1.0)
[Telegramer](https://github.com/noam09/deluge-telegramer) is a [Deluge](https://deluge-torrent.org) plugin for sending notifications, adding and viewing torrents using [Telegram](https://telegram.org/) messenger. It features both a GTK and Web UI.
@@ -20,7 +20,7 @@ Since the bot runs locally and is owned by the same user running it, Telegramer
## Requirements
-Currently Telegramer has been tested mainly on Linux using Deluge 1.3.15. Support for Windows is also available.
+Currently Telegramer has been tested mainly on Linux using Deluge 1.3.15. Support for Windows is also available. A [**beta** version](https://github.com/noam09/deluge-telegramer/releases/tag/2.1.1.0) of the plugin is available for Deluge 2, tested on v2.1.1.
Make sure you have Python [`setuptools`](https://pypi.python.org/pypi/setuptools#installation-instructions) installed in order to build the plugin.
The plugin itself works with the [`python-telegram-bot`](https://github.com/python-telegram-bot/python-telegram-bot) wrapper, which comes pre-packaged.
diff --git a/build-all.sh b/build-all.sh
new file mode 100755
index 0000000..261f4bc
--- /dev/null
+++ b/build-all.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+docker build -f Dockerfile-py --build-arg PYTHON_VERSION=${PYTHON_VERSION} --no-cache -t telegramer.build . \
+ && docker run -v $(pwd)/out:/tmp/out --rm -i telegramer.build sh -s << COMMANDS
+python setup.py bdist_egg
+chown -R $(id -u):$(id -g) dist
+cp -ar dist/ /tmp/out/
+COMMANDS
diff --git a/setup.py b/setup.py
index 1df357f..74c7b84 100755
--- a/setup.py
+++ b/setup.py
@@ -43,10 +43,11 @@
#
from setuptools import setup, find_packages
+
__plugin_name__ = "Telegramer"
__author__ = "Noam"
__author_email__ = "noamgit@gmail.com"
-__version__ = "1.3.1"
+__version__ = "2.1.1.3"
__url__ = "https://github.com/noam09"
__license__ = "GPLv3"
__description__ = "Control Deluge using Telegram"
@@ -68,14 +69,15 @@
long_description=__long_description__ if __long_description__ else __description__,
packages=packages,
package_data=__pkg_data__,
- entry_points="""
- [deluge.plugin.core]
- %s = %s:CorePlugin
- [deluge.plugin.gtkui]
- %s = %s:GtkUIPlugin
- [deluge.plugin.web]
- %s = %s:WebUIPlugin
- [telegramer.libpaths]
- include = telegramer.include
- """ % ((__plugin_name__, __plugin_name__.lower())*3)
+ entry_points="""[deluge.plugin.core]
+%s = %s:CorePlugin
+[deluge.plugin.gtkui]
+%s = %s:GtkUIPlugin
+[deluge.plugin.web]
+%s = %s:WebUIPlugin
+[deluge.plugin.gtk3ui]
+%s = %s:Gtk3UIPlugin
+[telegramer.libpaths]
+include = telegramer.include
+""" % ((__plugin_name__, __plugin_name__.lower())*4)
)
diff --git a/telegramer/__init__.py b/telegramer/__init__.py
index 41eb2dc..8adfc54 100755
--- a/telegramer/__init__.py
+++ b/telegramer/__init__.py
@@ -54,14 +54,15 @@ def load_libs():
for name in egg.get_entry_map("telegramer.libpaths"):
ep = egg.get_entry_info("telegramer.libpaths", name)
location = "%s/%s" % (egg.location, ep.module_name.replace(".", "/"))
- sys.path.append(location)
- log.error("Appending to sys.path: '%s'" % location)
+ if location not in sys.path:
+ sys.path.append(location)
+ log.error("NOTANERROR: Appending to sys.path: '%s'" % location)
class CorePlugin(PluginInitBase):
def __init__(self, plugin_name):
load_libs()
- from core import Core as _plugin_cls
+ from .core import Core as _plugin_cls
self._plugin_cls = _plugin_cls
super(CorePlugin, self).__init__(plugin_name)
@@ -69,7 +70,7 @@ def __init__(self, plugin_name):
class GtkUIPlugin(PluginInitBase):
def __init__(self, plugin_name):
load_libs()
- from gtkui import GtkUI as _plugin_cls
+ from .gtkui import GtkUI as _plugin_cls
self._plugin_cls = _plugin_cls
super(GtkUIPlugin, self).__init__(plugin_name)
@@ -77,6 +78,16 @@ def __init__(self, plugin_name):
class WebUIPlugin(PluginInitBase):
def __init__(self, plugin_name):
load_libs()
- from webui import WebUI as _plugin_cls
+ from .webui import WebUI as _plugin_cls
self._plugin_cls = _plugin_cls
super(WebUIPlugin, self).__init__(plugin_name)
+
+
+class Gtk3UIPlugin(PluginInitBase):
+ def __init__(self, plugin_name):
+ load_libs()
+ from .gtk3ui import Gtk3UI as GtkUIPluginClass
+ self._plugin_cls = GtkUIPluginClass
+ super(Gtk3UIPlugin, self).__init__(plugin_name)
+
+
diff --git a/telegramer/core.py b/telegramer/core.py
index b39cedf..ba97835 100755
--- a/telegramer/core.py
+++ b/telegramer/core.py
@@ -47,7 +47,10 @@
import logging
import traceback
from time import strftime
-from deluge.log import LOG as log
+import time
+# from deluge.log import LOG as log
+import logging
+log = logging.getLogger(__name__)
# import sys
# reload(sys)
@@ -64,10 +67,10 @@ def prelog():
try:
import re
- import urllib2
- from telegram import (ReplyKeyboardMarkup, ReplyKeyboardRemove, Bot)
- from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters,
- RegexHandler, ConversationHandler, BaseFilter)
+ import urllib.request, urllib.error, urllib.parse
+ from telegram import (ReplyKeyboardMarkup, ReplyKeyboardRemove, Bot, Update)
+ from telegram.ext import (Updater, CallbackContext, CommandHandler, MessageHandler,
+ ConversationHandler, Filters)
from telegram.utils.request import Request
import threading
from base64 import b64encode
@@ -87,12 +90,12 @@ def prelog():
log.error(prelog() + 'Import error - %s\n%s' % (str(e), traceback.format_exc()))
-class FilterMagnets(BaseFilter):
- def filter(self, message):
- return 'magnet:?' in message.text
+# class _FilterMagnets(BaseFilter):
+# def filter(self, message):
+# return 'magnet:?' in message.text
-CATEGORY, SET_LABEL, TORRENT_TYPE, ADD_MAGNET, ADD_TORRENT, ADD_URL, RSS_FEED, FILE_NAME, REGEX = range(9)
+CATEGORY, SET_LABEL, TORRENT_TYPE, ADD_MAGNET, ADD_TORRENT, ADD_URL, RSS_FEED, FILE_NAME, REGEX = list(range(9))
DEFAULT_PREFS = {"telegram_token": "Contact @BotFather and create a new bot",
"telegram_user": "Contact @MyIDbot",
@@ -122,11 +125,11 @@ def filter(self, message):
'snow': 'CAADAgADZQUAAgi3GQJyjRNCuIA54gI',
'borat': 'CAADBAADmwQAAjJQbQAB5DpM4iETWoQC'}
-EMOJI = {'seeding': u'\u23eb',
- 'queued': u'\u23ef',
- 'paused': u'\u23f8',
- 'error': u'\u2757\ufe0f',
- 'downloading': u'\u23ec'}
+EMOJI = {'seeding': '\u23eb',
+ 'queued': '\u23ef',
+ 'paused': '\u23f8',
+ 'error': '\u2757\ufe0f',
+ 'downloading': '\u23ec'}
REGEX_SUBS_WORD = r"NAME"
@@ -163,7 +166,7 @@ def filter(self, message):
INFO_DICT = (('queue', lambda i, s: i != -1 and str(i) or '#'),
('state', None),
- ('name', lambda i, s: u' %s *%s* ' %
+ ('name', lambda i, s: ' %s *%s* ' %
(s['state'] if s['state'].lower() not in EMOJI
else EMOJI[s['state'].lower()],
i)),
@@ -183,7 +186,7 @@ def filter(self, message):
INFOS = [i[0] for i in INFO_DICT]
-filter_magnets = FilterMagnets()
+# filter_magnets = _FilterMagnets()
def is_int(s):
@@ -200,7 +203,7 @@ def format_torrent_info(torrent):
log.debug(status)
status_string = ''
try:
- status_string = u''.join([f(status[i], status) for i, f in INFO_DICT if f is not None])
+ status_string = ''.join([f(status[i], status) for i, f in INFO_DICT if f is not None])
# except UnicodeDecodeError as e:
except Exception as e:
status_string = ''
@@ -240,7 +243,7 @@ def enable(self):
'seeding': self.cmd_up,
'paused': self.cmd_paused,
'queued': self.cmd_paused,
- '?': self.cmd_help,
+ # '?': self.cmd_help,
'cancel': self.cancel,
'help': self.cmd_help,
'start': self.cmd_help,
@@ -248,7 +251,8 @@ def enable(self):
# 'rss': self.cmd_add_rss,
'commands': self.cmd_help}
- log.debug(prelog() + 'Initialize bot')
+ self.torrent_manager = component.get("TorrentManager")
+ log.info(prelog() + 'Initialize bot')
if self.config['telegram_token'] != DEFAULT_PREFS['telegram_token']:
if self.config['telegram_user']:
@@ -256,14 +260,14 @@ def enable(self):
self.whitelist.append(str(self.config['telegram_user']))
self.notifylist.append(str(self.config['telegram_user']))
if self.config['telegram_users']:
- telegram_user_list = filter(None, [x.strip() for x in
- str(self.config['telegram_users']).split(',')])
+ telegram_user_list = [_f for _f in [x.strip() for x in
+ str(self.config['telegram_users']).split(',')] if _f]
# Merge with whitelist and remove duplicates - order will be lost
self.whitelist = list(set(self.whitelist + telegram_user_list))
log.debug(prelog() + 'Whitelist: ' + str(self.whitelist))
if self.config['telegram_users_notify']:
- n = filter(None, [x.strip() for x in
- str(self.config['telegram_users_notify']).split(',')])
+ n = [_f for _f in [x.strip() for x in
+ str(self.config['telegram_users_notify']).split(',')] if _f]
telegram_user_list_notify = [a for a in n if is_int(a)]
# Merge with notifylist and remove duplicates - order will be lost
self.notifylist = list(set(self.notifylist +
@@ -290,7 +294,7 @@ def enable(self):
self.bot = Bot(self.config['telegram_token'], request=bot_request)
# Create the EventHandler and pass it bot's token.
# self.updater = Updater(self.config['telegram_token'], bot=self.bot, request_kwargs=REQUEST_KWARGS)
- self.updater = Updater(bot=self.bot, request_kwargs=REQUEST_KWARGS)
+ self.updater = Updater(bot=self.bot, use_context=True, request_kwargs=REQUEST_KWARGS)
# Get the dispatcher to register handlers
dp = self.updater.dispatcher
# Add conversation handler with the different states
@@ -334,9 +338,9 @@ def enable(self):
dp.add_handler(conv_handler_paused)
dp.add_handler(conv_handler_rss)
dp.add_handler(MessageHandler(Filters.document, self.add_torrent))
- dp.add_handler(MessageHandler(filter_magnets, self.find_magnet))
+ dp.add_handler(MessageHandler(Filters.regex(r'magnet:\?'), self.find_magnet))
- for key, value in self.COMMANDS.iteritems():
+ for key, value in self.COMMANDS.items():
dp.add_handler(CommandHandler(key, value))
# Log all errors
@@ -358,8 +362,8 @@ def enable(self):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def error(self, bot, update, error):
- log.warn('Update "%s" caused error "%s"' % (update, error))
+ def error(self, update: Update, context: CallbackContext):
+ log.warn('Update "%s" caused error "%s"' % (update, context.error))
def disable(self):
try:
@@ -384,7 +388,7 @@ def telegram_send(self, message, to=None, parse_mode=None):
to = self.config['telegram_user']
else:
log.debug(prelog() + 'send_message, to set')
- if not isinstance(to, (list,)):
+ if not isinstance(to, list):
log.debug(prelog() + 'Convert "to" to list')
to = [to]
log.debug(prelog() + "[to] " + str(to))
@@ -445,7 +449,7 @@ def telegram_poll_stop(self):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def cancel(self, bot, update):
+ def cancel(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
log.info("User %s canceled the conversation."
% str(update.message.chat.id))
@@ -456,7 +460,7 @@ def cancel(self, bot, update):
self.yarss_data.clear()
return ConversationHandler.END
- def cmd_help(self, bot, update):
+ def cmd_help(self, update: Update, context: CallbackContext):
log.debug(prelog() + "Entered cmd_help")
if str(update.message.chat.id) in self.whitelist:
log.debug(prelog() + str(update.message.chat.id) + " in whitelist")
@@ -475,7 +479,7 @@ def cmd_help(self, bot, update):
to=[update.message.chat.id],
parse_mode='Markdown')
- def cmd_list(self, bot, update):
+ def cmd_list(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
# log.error(self.list_torrents())
self.telegram_send(self.list_torrents(lambda t:
@@ -485,21 +489,21 @@ def cmd_list(self, bot, update):
to=[update.message.chat.id],
parse_mode='Markdown')
- def cmd_down(self, bot, update):
+ def cmd_down(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
self.telegram_send(self.list_torrents(lambda t:
t.get_status(('state',))['state'] == 'Downloading'),
to=[update.message.chat.id],
parse_mode='Markdown')
- def cmd_up(self, bot, update):
+ def cmd_up(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
self.telegram_send(self.list_torrents(lambda t:
t.get_status(('state',))['state'] == 'Seeding'),
to=[update.message.chat.id],
parse_mode='Markdown')
- def cmd_paused(self, bot, update):
+ def cmd_paused(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
self.telegram_send(self.list_torrents(lambda t:
t.get_status(('state',))['state'] in
@@ -507,43 +511,42 @@ def cmd_paused(self, bot, update):
to=[update.message.chat.id],
parse_mode='Markdown')
- def add(self, bot, update):
- # log.error(type(update.message.chat.id) + str(update.message.chat.id))
+ def add(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
self.opts = {}
self.is_rss = False
self.magnet_only = False
- return self.prepare_categories(bot, update)
+ return self.prepare_categories(update, context)
"""
if "YaRSS2" in component.get('Core').get_available_plugins():
- return self.prepare_torrent_or_rss(bot, update)
+ return self.prepare_torrent_or_rss(update, context)
else:
- return self.prepare_categories(bot, update)
+ return self.prepare_categories(update, context)
"""
- def add_paused(self, bot, update):
+ def add_paused(self, update: Update, context: CallbackContext):
# log.error(type(update.message.chat.id) + str(update.message.chat.id))
if str(update.message.chat.id) in self.whitelist:
self.opts = {}
self.opts["addpaused"] = True
self.is_rss = False
self.magnet_only = False
- return self.prepare_categories(bot, update)
+ return self.prepare_categories(update, context)
- def cmd_add_rss(self, bot, update):
+ def cmd_add_rss(self, update: Update, context: CallbackContext):
# log.error(type(update.message.chat.id) + str(update.message.chat.id))
if str(update.message.chat.id) in self.whitelist:
if "YaRSS2" in component.get('Core').get_available_plugins():
- return self.add_rss(bot, update)
+ return self.add_rss(update, context)
else:
update.message.reply_text('YaRSS2 plugin not available',
reply_markup=ReplyKeyboardRemove())
self.is_rss = False
self.yarss_data.clear()
return ConversationHandler.END
- # return self.prepare_categories(bot, update)
+ # return self.prepare_categories(update, context)
- # def prepare_torrent_or_rss(self, bot, update):
+ # def prepare_torrent_or_rss(self, update: Update, context: CallbackContext):
# try:
# keyboard_options = [[STRINGS['torrent']], [STRINGS['rss']]]
# update.message.reply_text(
@@ -555,29 +558,29 @@ def cmd_add_rss(self, bot, update):
# except Exception as e:
# log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def torrent_or_rss(self, bot, update):
+ def torrent_or_rss(self, update: Update, context: CallbackContext):
try:
if str(update.message.chat.id) not in self.whitelist:
return
if STRINGS['torrent'] == update.message.text:
- return self.prepare_categories(bot, update)
+ return self.prepare_categories(update, context)
if STRINGS['rss'] == update.message.text:
- return self.add_rss(bot, update)
+ return self.add_rss(update, context)
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def prepare_categories(self, bot, update):
+ def prepare_categories(self, update: Update, context: CallbackContext):
try:
keyboard_options = []
- filtered_dict = {c: d for c, d in self.config["categories"].iteritems() if os.path.isdir(d)}
- missing = {c: d for c, d in self.config["categories"].iteritems() if not os.path.isdir(d)}
+ filtered_dict = {c: d for c, d in self.config["categories"].items() if os.path.isdir(d)}
+ missing = {c: d for c, d in self.config["categories"].items() if not os.path.isdir(d)}
if len(missing) > 0:
- for k in missing.keys():
+ for k in list(missing.keys()):
log.error(prelog() + "Missing directory for category {} ({})".format(k, missing[k]))
- for c, d in filtered_dict.iteritems():
+ for c, d in filtered_dict.items():
log.error(prelog() + c + ' : ' + d)
keyboard_options.append([c])
@@ -590,7 +593,7 @@ def prepare_categories(self, bot, update):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def prepare_torrent_type(self, update):
+ def prepare_torrent_type(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
try:
# Request torrent type
@@ -607,13 +610,13 @@ def prepare_torrent_type(self, update):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def category(self, bot, update):
+ def category(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
try:
if STRINGS['no_category'] == update.message.text:
self.opts = self.opts
else:
- if update.message.text in self.config["categories"].keys():
+ if update.message.text in list(self.config["categories"].keys()):
# move_completed_path vs download_location
self.opts["move_completed_path"] = self.config["categories"][update.message.text]
self.opts["move_completed"] = True
@@ -641,7 +644,7 @@ def category(self, bot, update):
traceback.format_exc())
if self.is_rss:
log.debug(prelog() + "is_rss, calling RSS_APPLY")
- return self.rss_apply(bot, update)
+ return self.rss_apply(update, context)
log.debug(prelog() + "Label segment")
keyboard_options = []
@@ -676,7 +679,7 @@ def category(self, bot, update):
return SET_LABEL
else:
if self.is_rss:
- return self.prepare_rss_feed(bot, update)
+ return self.prepare_rss_feed(update, context)
else:
return self.prepare_torrent_type(update)
"""
@@ -684,21 +687,21 @@ def category(self, bot, update):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def prepare_rss_feed(self, bot, update):
+ def prepare_rss_feed(self, update: Update, context: CallbackContext):
if self.yarss_plugin is None:
self.yarss_plugin = component.get('CorePlugin.YaRSS2')
if self.yarss_plugin:
self.yarss_config = self.yarss_plugin.get_config()
feeds = {}
# Create dictionary with feed name:url
- for rss_feed in self.yarss_config["rssfeeds"].values():
+ for rss_feed in list(self.yarss_config["rssfeeds"].values()):
feeds[rss_feed["name"]] = rss_feed["url"]
# If feed(s) found
count = 0
if len(feeds) > 0:
count = count + 1
- feedlist = "\n".join(["{}) [{}]({})".format(count, f, feeds[f]) for f in feeds.keys()])
- keyboard_options = [feeds.keys()]
+ feedlist = "\n".join(["{}) [{}]({})".format(count, f, feeds[f]) for f in list(feeds.keys())])
+ keyboard_options = [list(feeds.keys())]
update.message.reply_text(
'%s\n\n%s\n\n%s' % (STRINGS['which_rss_feed'], feedlist, STRINGS['cancel']),
reply_markup=ReplyKeyboardMarkup(keyboard_options, one_time_keyboard=True),
@@ -713,12 +716,12 @@ def prepare_rss_feed(self, bot, update):
return ConversationHandler.END
- def rss_feed(self, bot, update):
+ def rss_feed(self, update: Update, context: CallbackContext):
if not str(update.message.chat.id) in self.whitelist:
return
self.is_rss = True
try:
- rss_feed = next(rss_feed for rss_feed in self.yarss_config["rssfeeds"].values()
+ rss_feed = next(rss_feed for rss_feed in list(self.yarss_config["rssfeeds"].values())
if rss_feed["name"] == update.message.text)
self.yarss_data.subscription_data["rssfeed_key"] = rss_feed["key"]
@@ -726,7 +729,7 @@ def rss_feed(self, bot, update):
log.debug(self.config["regex_exp"])
- keyboard_options = [[regex_name] for regex_name in self.config["regex_exp"].keys() if regex_name != '']
+ keyboard_options = [[regex_name] for regex_name in list(self.config["regex_exp"].keys()) if regex_name != '']
if len(keyboard_options) > 0:
update.message.reply_text(
@@ -741,7 +744,7 @@ def rss_feed(self, bot, update):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def regex(self, bot, update):
+ def regex(self, update: Update, context: CallbackContext):
if not str(update.message.chat.id) in self.whitelist:
return
if REGEX_SUBS_WORD in self.config["regex_exp"][update.message.text]:
@@ -753,13 +756,13 @@ def regex(self, bot, update):
reply_markup=ReplyKeyboardRemove())
return FILE_NAME
else:
- keyboard_options = [[regex_name] for regex_name in self.config["regex_exp"].keys()]
+ keyboard_options = [[regex_name] for regex_name in list(self.config["regex_exp"].keys())]
update.message.reply_text(
'%s\n%s' % (STRINGS['no_name'], STRINGS['cancel']),
reply_markup=ReplyKeyboardMarkup(keyboard_options, one_time_keyboard=True))
return REGEX
- def rss_file_name(self, bot, update):
+ def rss_file_name(self, update: Update, context: CallbackContext):
if not str(update.message.chat.id) in self.whitelist:
return
@@ -775,9 +778,9 @@ def rss_file_name(self, bot, update):
self.yarss_data.subscription_data["label"] = self.label
self.yarss_data.subscription_data["name"] = update.message.text
- return self.prepare_categories(bot, update)
+ return self.prepare_categories(update, context)
- def rss_apply(self, bot, update):
+ def rss_apply(self, update: Update, context: CallbackContext):
log.debug(prelog() + "entered rss_apply")
if str(update.message.chat.id) in self.whitelist:
log.debug(prelog() + "in whitelist")
@@ -796,19 +799,19 @@ def rss_apply(self, bot, update):
else:
log.debug(prelog() + "not in whitelist")
- def set_label(self, bot, update):
+ def set_label(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
try:
# user = update.message.chat.id
self.label = update.message.text
log.debug(prelog() + "Label: %s" % (update.message.text))
- return self.prepare_torrent_type(update)
+ return self.prepare_torrent_type(update, context)
"""
# Request torrent type
if self.is_rss:
- return self.prepare_rss_feed(bot, update)
+ return self.prepare_rss_feed(update, context)
else:
return self.prepare_torrent_type(update)
"""
@@ -816,7 +819,7 @@ def set_label(self, bot, update):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def torrent_type(self, bot, update):
+ def torrent_type(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
try:
user = update.message.chat.id
@@ -845,7 +848,7 @@ def torrent_type(self, bot, update):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def add_magnet(self, bot, update):
+ def add_magnet(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
try:
if self.magnet_only:
@@ -891,7 +894,7 @@ def add_magnet(self, bot, update):
# except Exception as e:
# log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def find_magnet(self, bot, update):
+ def find_magnet(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
try:
log.debug("Find magnets in message")
@@ -901,7 +904,7 @@ def find_magnet(self, bot, update):
if len(m) > 0:
mag = m[0]
self.magnet_only = True
- return self.add_magnet(bot, update)
+ return self.add_magnet(update, context)
else:
log.debug("Magnet not found in message")
update.message.reply_text(STRINGS['no_magnet_found'],
@@ -915,7 +918,7 @@ def find_magnet(self, bot, update):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def add_torrent(self, bot, update):
+ def add_torrent(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
try:
user = update.message.chat.id
@@ -926,10 +929,10 @@ def add_torrent(self, bot, update):
# Get file info
file_info = self.bot.getFile(update.message.document.file_id)
# Download file
- request = urllib2.Request(file_info.file_path, headers=HEADERS)
- status_code = urllib2.urlopen(request).getcode()
+ request = urllib.request.Request(file_info.file_path, headers=HEADERS)
+ status_code = urllib.request.urlopen(request).getcode()
if status_code == 200:
- file_contents = urllib2.urlopen(request).read()
+ file_contents = urllib.request.urlopen(request).read()
# Base64 encode file data
metainfo = b64encode(file_contents)
if self.opts is None:
@@ -950,7 +953,7 @@ def add_torrent(self, bot, update):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def add_url(self, bot, update):
+ def add_url(self, update: Update, context: CallbackContext):
if str(update.message.chat.id) in self.whitelist:
try:
user = update.message.chat.id
@@ -959,11 +962,11 @@ def add_url(self, bot, update):
if is_url(update.message.text):
try:
# Download file
- request = urllib2.Request(update.message.text.strip(),
+ request = urllib.request.Request(update.message.text.strip(),
headers=HEADERS)
- status_code = urllib2.urlopen(request).getcode()
+ status_code = urllib.request.urlopen(request).getcode()
if status_code == 200:
- file_contents = urllib2.urlopen(request).read()
+ file_contents = urllib.request.urlopen(request).read()
# Base64 encode file data
metainfo = b64encode(file_contents)
if self.opts is None:
@@ -988,12 +991,12 @@ def add_url(self, bot, update):
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
- def add_rss(self, bot, update):
+ def add_rss(self, update: Update, context: CallbackContext):
try:
component.get('Core').enable_plugin('YaRSS2')
self.is_rss = True
- return self.prepare_rss_feed(bot, update)
- # return self.prepare_categories(bot, update)
+ return self.prepare_rss_feed(update, context)
+ # return self.prepare_categories(update, context)
except Exception as e:
log.error(prelog() + str(e) + '\n' + traceback.format_exc())
@@ -1021,7 +1024,7 @@ def update_stats(self):
def check_speed(self):
log.debug("Minimum speed: %s", self.config["minimum_speed"])
try:
- for t in component.get('TorrentManager').torrents.values():
+ for t in list(component.get('TorrentManager').torrents.values()):
if t.get_status(("state",))["state"] == "Downloading":
if t.status.download_rate < (self.config['minimum_speed'] * 1024):
message = _('Torrent *%(name)s* is slower than minimum speed!') % t.get_status({})
@@ -1044,19 +1047,29 @@ def disconnect_events(self):
event_manager.deregister_event_handler('TorrentAddedEvent',
self.on_torrent_added)
- def on_torrent_added(self, torrent_id):
+ def on_torrent_added(self, torrent_id, from_state):
if (self.config['telegram_notify_added'] is False):
return
try:
custom_message = False
- # get_torrent_status
- # torrent_id = str(alert.handle.info_hash())
torrent = component.get('TorrentManager')[torrent_id]
- torrent_status = torrent.get_status({})
+ torrent_status = torrent.get_status(['name'])
+ # get the torrent added time from get_status
+ torrent_added = torrent.get_status(['time_added'])
+ # check if torrent_added time is in the last 5 minutes
+ # if it is, send the message
+ # if it is not, do not send the message
+ # this is to prevent the bot from sending a message when the bot is restarted
+ # and all the torrents are added
+ # this is a hacky way to do it, but it works
+ # if the torrent was added more than 5 minutes ago, do not send the message
+ if (torrent_added["time_added"] < (time.time() - 300)):
+ return
# Check if label shows up here
log.debug("get_status for {}".format(torrent_id))
log.debug(torrent_status)
message = _('Added Torrent *%(name)s*') % torrent_status
+ log.info(prelog() + 'Torrent added: %s' % torrent_added)
# Check if custom message
if self.config["message_added"] is not DEFAULT_PREFS["message_added"] \
and len(self.config["message_added"]) > 0:
@@ -1076,12 +1089,9 @@ def on_torrent_finished(self, torrent_id):
try:
if (self.config['telegram_notify_finished'] is False):
return
- # torrent_id = str(alert.handle.info_hash())
custom_message = False
- # get_torrent_status
- # torrent_id = str(alert.handle.info_hash())
torrent = component.get('TorrentManager')[torrent_id]
- torrent_status = torrent.get_status({})
+ torrent_status = torrent.get_status(['name'])
# Check if label shows up here
log.debug("get_status for {}".format(torrent_id))
log.debug(torrent_status)
@@ -1102,18 +1112,23 @@ def on_torrent_finished(self, torrent_id):
log.error(prelog() + 'Error in alert %s' %
str(e) + '\n' + traceback.format_exc())
- def list_torrents(self, filter=lambda _: True):
- return '\n'.join([format_torrent_info(t) for t
- in component.get('TorrentManager').torrents.values()
- if filter(t)] or [STRINGS['no_items']])
+ def list_torrents(self, filterz=lambda _: True):
+ selected_torrents = []
+ torrents = list(self.torrent_manager.torrents.values())
+ for t in torrents:
+ if filterz(t):
+ selected_torrents.append(format_torrent_info(t))
+ if len(selected_torrents) == 0:
+ return STRINGS['no_items']
+ return "\n".join(selected_torrents)
@export
def set_config(self, config):
"""Sets the config dictionary"""
log.debug(prelog() + 'Set config')
dirty = False
- for key in config.keys():
- if ("categories" == key and cmp(self.config[key], config[key])) or \
+ for key in list(config.keys()):
+ if ("categories" == key and self.config[key] == config[key]) or \
self.config[key] != config[key]:
dirty = True
self.config[key] = config[key]
diff --git a/telegramer/data/config.ui b/telegramer/data/config.ui
new file mode 100644
index 0000000..5333426
--- /dev/null
+++ b/telegramer/data/config.ui
@@ -0,0 +1,1001 @@
+
+
+
+
+
+
+
diff --git a/telegramer/gtk3ui.py b/telegramer/gtk3ui.py
new file mode 100755
index 0000000..6d09954
--- /dev/null
+++ b/telegramer/gtk3ui.py
@@ -0,0 +1,148 @@
+#
+# gtkui.py
+#
+# Copyright (C) 2016-2019 Noam
+# https://github.com/noam09
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken
+# Copyright (C) 2007-2009 Andrew Resch
+# Copyright (C) 2009 Damien Churchill
+#
+# Deluge is free software.
+#
+# You may redistribute it and/or modify it under the terms of the
+# GNU General Public License, as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option)
+# any later version.
+#
+# deluge is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with deluge. If not, write to:
+# The Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+#
+
+
+try:
+ from deluge.log import LOG as log
+except Exception as e:
+ print('Telegramer: Exception - %s' % str(e))
+
+try:
+ from gi.repository import Gtk
+ import deluge.common
+ from .common import get_resource
+ from deluge.ui.client import client
+ import deluge.component as component
+ from deluge.plugins.pluginbase import Gtk3PluginBase
+except ImportError as e:
+ log.error('Telegramer: Import error - %s', str(e))
+
+REGEX_SUBS_WORD = "NAME"
+
+
+class Gtk3UI(Gtk3PluginBase):
+ def enable(self):
+ self.builder = Gtk.Builder.new_from_file(get_resource("config.ui"))
+ self.builder.connect_signals({
+ "on_button_test_clicked": self.on_button_test_clicked,
+ "on_button_save_clicked": self.on_button_save_clicked,
+ "on_button_reload_clicked": self.on_button_reload_clicked
+ })
+ component.get("Preferences").add_page("Telegramer", self.builder.get_object("prefs_box"))
+ component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs)
+ component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs)
+
+ def disable(self):
+ component.get("Preferences").remove_page("Telegramer")
+ component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs)
+ component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs)
+
+ def on_apply_prefs(self):
+ log.debug("Telegramer: applying prefs for Telegramer")
+ config = {
+ "telegram_notify_added": self.builder.get_object("telegram_notify_added").get_active(),
+ "telegram_notify_finished": self.builder.get_object("telegram_notify_finished").get_active(),
+ "telegram_token": self.builder.get_object("telegram_token").get_text(),
+ "telegram_user": self.builder.get_object("telegram_user").get_text(),
+ "telegram_users": self.builder.get_object("telegram_users").get_text(),
+ "telegram_users_notify": self.builder.get_object("telegram_users_notify").get_text(),
+ "minimum_speed": self.builder.get_object("minimum_speed").get_text(),
+ "user_timer": self.builder.get_object("user_timer").get_text(),
+ "proxy_url": self.builder.get_object("proxy_url").get_text(),
+ "urllib3_proxy_kwargs_username": self.builder.get_object("urllib3_proxy_kwargs_username").get_text(),
+ "urllib3_proxy_kwargs_password": self.builder.get_object("urllib3_proxy_kwargs_password").get_text(),
+ "categories": {self.builder.get_object("cat1").get_text():
+ self.builder.get_object("dir1").get_text(),
+ self.builder.get_object("cat2").get_text():
+ self.builder.get_object("dir2").get_text(),
+ self.builder.get_object("cat3").get_text():
+ self.builder.get_object("dir3").get_text()
+ },
+ "regex_exp": {self.builder.get_object("rname1").get_text():
+ self.builder.get_object("reg1").get_text(),
+ self.builder.get_object("rname2").get_text():
+ self.builder.get_object("reg2").get_text(),
+ self.builder.get_object("rname3").get_text():
+ self.builder.get_object("reg3").get_text()
+ }
+ }
+
+ client.telegramer.set_config(config)
+ for ind, (n, r) in enumerate(config["regex_exp"].items()):
+ if REGEX_SUBS_WORD not in r:
+ log.error("Your regex " + n + " template does not contain the "
+ + REGEX_SUBS_WORD + " keyword")
+ break
+
+ def on_show_prefs(self):
+ client.telegramer.get_config().addCallback(self.cb_get_config)
+
+ def cb_get_config(self, config):
+ "callback for on show_prefs"
+ self.builder.get_object("telegram_notify_added").set_active(config["telegram_notify_added"])
+ self.builder.get_object("telegram_notify_finished").set_active(config["telegram_notify_finished"])
+ self.builder.get_object("telegram_token").set_text(config["telegram_token"])
+ self.builder.get_object("telegram_user").set_text(config["telegram_user"])
+ self.builder.get_object("telegram_users").set_text(config["telegram_users"])
+ self.builder.get_object("telegram_users_notify").set_text(config["telegram_users_notify"])
+ # Slow
+ self.builder.get_object("minimum_speed").set_text(str(config["minimum_speed"]))
+ self.builder.get_object("user_timer").set_text(str(config["user_timer"]))
+ # Proxy
+ self.builder.get_object("proxy_url").set_text(config["proxy_url"]),
+ self.builder.get_object('urllib3_proxy_kwargs_username').set_text(config["urllib3_proxy_kwargs_username"]),
+ self.builder.get_object("urllib3_proxy_kwargs_password").set_text(config["urllib3_proxy_kwargs_password"]),
+ # Categories
+ for ind, (c, d) in enumerate(config["categories"].items()):
+ self.builder.get_object("cat"+str(ind+1)).set_text(c)
+ self.builder.get_object("dir"+str(ind+1)).set_text(d)
+ # RSS
+ for ind, (n, r) in enumerate(config["regex_exp"].items()):
+ self.builder.get_object("rname"+str(ind+1)).set_text(n)
+ self.builder.get_object("reg"+str(ind+1)).set_text(r)
+
+ def on_button_test_clicked(self, Event=None):
+ client.telegramer.telegram_do_test()
+
+ def on_button_save_clicked(self, Event=None):
+ self.on_apply_prefs()
+
+ def on_button_reload_clicked(self, Event=None):
+ client.telegramer.restart_telegramer()
diff --git a/telegramer/gtkui.py b/telegramer/gtkui.py
index a3f1f50..9570a26 100755
--- a/telegramer/gtkui.py
+++ b/telegramer/gtkui.py
@@ -42,7 +42,7 @@
try:
from deluge.log import LOG as log
except Exception as e:
- print 'Telegramer: Exception - %s' % str(e)
+ print("Telegramer: Exception - {}".format(str(e)))
try:
import gtk
diff --git a/telegramer/include/apscheduler/__init__.py b/telegramer/include/apscheduler/__init__.py
new file mode 100644
index 0000000..968169a
--- /dev/null
+++ b/telegramer/include/apscheduler/__init__.py
@@ -0,0 +1,10 @@
+from pkg_resources import get_distribution, DistributionNotFound
+
+try:
+ release = get_distribution('APScheduler').version.split('-')[0]
+except DistributionNotFound:
+ release = '3.5.0'
+
+version_info = tuple(int(x) if x.isdigit() else x for x in release.split('.'))
+version = __version__ = '.'.join(str(x) for x in version_info[:3])
+del get_distribution, DistributionNotFound
diff --git a/telegramer/include/apscheduler/events.py b/telegramer/include/apscheduler/events.py
new file mode 100644
index 0000000..016da03
--- /dev/null
+++ b/telegramer/include/apscheduler/events.py
@@ -0,0 +1,94 @@
+__all__ = ('EVENT_SCHEDULER_STARTED', 'EVENT_SCHEDULER_SHUTDOWN', 'EVENT_SCHEDULER_PAUSED',
+ 'EVENT_SCHEDULER_RESUMED', 'EVENT_EXECUTOR_ADDED', 'EVENT_EXECUTOR_REMOVED',
+ 'EVENT_JOBSTORE_ADDED', 'EVENT_JOBSTORE_REMOVED', 'EVENT_ALL_JOBS_REMOVED',
+ 'EVENT_JOB_ADDED', 'EVENT_JOB_REMOVED', 'EVENT_JOB_MODIFIED', 'EVENT_JOB_EXECUTED',
+ 'EVENT_JOB_ERROR', 'EVENT_JOB_MISSED', 'EVENT_JOB_SUBMITTED', 'EVENT_JOB_MAX_INSTANCES',
+ 'SchedulerEvent', 'JobEvent', 'JobExecutionEvent', 'JobSubmissionEvent')
+
+
+EVENT_SCHEDULER_STARTED = EVENT_SCHEDULER_START = 2 ** 0
+EVENT_SCHEDULER_SHUTDOWN = 2 ** 1
+EVENT_SCHEDULER_PAUSED = 2 ** 2
+EVENT_SCHEDULER_RESUMED = 2 ** 3
+EVENT_EXECUTOR_ADDED = 2 ** 4
+EVENT_EXECUTOR_REMOVED = 2 ** 5
+EVENT_JOBSTORE_ADDED = 2 ** 6
+EVENT_JOBSTORE_REMOVED = 2 ** 7
+EVENT_ALL_JOBS_REMOVED = 2 ** 8
+EVENT_JOB_ADDED = 2 ** 9
+EVENT_JOB_REMOVED = 2 ** 10
+EVENT_JOB_MODIFIED = 2 ** 11
+EVENT_JOB_EXECUTED = 2 ** 12
+EVENT_JOB_ERROR = 2 ** 13
+EVENT_JOB_MISSED = 2 ** 14
+EVENT_JOB_SUBMITTED = 2 ** 15
+EVENT_JOB_MAX_INSTANCES = 2 ** 16
+EVENT_ALL = (EVENT_SCHEDULER_STARTED | EVENT_SCHEDULER_SHUTDOWN | EVENT_SCHEDULER_PAUSED |
+ EVENT_SCHEDULER_RESUMED | EVENT_EXECUTOR_ADDED | EVENT_EXECUTOR_REMOVED |
+ EVENT_JOBSTORE_ADDED | EVENT_JOBSTORE_REMOVED | EVENT_ALL_JOBS_REMOVED |
+ EVENT_JOB_ADDED | EVENT_JOB_REMOVED | EVENT_JOB_MODIFIED | EVENT_JOB_EXECUTED |
+ EVENT_JOB_ERROR | EVENT_JOB_MISSED | EVENT_JOB_SUBMITTED | EVENT_JOB_MAX_INSTANCES)
+
+
+class SchedulerEvent(object):
+ """
+ An event that concerns the scheduler itself.
+
+ :ivar code: the type code of this event
+ :ivar alias: alias of the job store or executor that was added or removed (if applicable)
+ """
+
+ def __init__(self, code, alias=None):
+ super(SchedulerEvent, self).__init__()
+ self.code = code
+ self.alias = alias
+
+ def __repr__(self):
+ return '<%s (code=%d)>' % (self.__class__.__name__, self.code)
+
+
+class JobEvent(SchedulerEvent):
+ """
+ An event that concerns a job.
+
+ :ivar code: the type code of this event
+ :ivar job_id: identifier of the job in question
+ :ivar jobstore: alias of the job store containing the job in question
+ """
+
+ def __init__(self, code, job_id, jobstore):
+ super(JobEvent, self).__init__(code)
+ self.code = code
+ self.job_id = job_id
+ self.jobstore = jobstore
+
+
+class JobSubmissionEvent(JobEvent):
+ """
+ An event that concerns the submission of a job to its executor.
+
+ :ivar scheduled_run_times: a list of datetimes when the job was intended to run
+ """
+
+ def __init__(self, code, job_id, jobstore, scheduled_run_times):
+ super(JobSubmissionEvent, self).__init__(code, job_id, jobstore)
+ self.scheduled_run_times = scheduled_run_times
+
+
+class JobExecutionEvent(JobEvent):
+ """
+ An event that concerns the running of a job within its executor.
+
+ :ivar scheduled_run_time: the time when the job was scheduled to be run
+ :ivar retval: the return value of the successfully executed job
+ :ivar exception: the exception raised by the job
+ :ivar traceback: a formatted traceback for the exception
+ """
+
+ def __init__(self, code, job_id, jobstore, scheduled_run_time, retval=None, exception=None,
+ traceback=None):
+ super(JobExecutionEvent, self).__init__(code, job_id, jobstore)
+ self.scheduled_run_time = scheduled_run_time
+ self.retval = retval
+ self.exception = exception
+ self.traceback = traceback
diff --git a/telegramer/include/apscheduler/executors/__init__.py b/telegramer/include/apscheduler/executors/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/telegramer/include/apscheduler/executors/asyncio.py b/telegramer/include/apscheduler/executors/asyncio.py
new file mode 100644
index 0000000..06fc7f9
--- /dev/null
+++ b/telegramer/include/apscheduler/executors/asyncio.py
@@ -0,0 +1,59 @@
+from __future__ import absolute_import
+
+import sys
+
+from apscheduler.executors.base import BaseExecutor, run_job
+from apscheduler.util import iscoroutinefunction_partial
+
+try:
+ from apscheduler.executors.base_py3 import run_coroutine_job
+except ImportError:
+ run_coroutine_job = None
+
+
+class AsyncIOExecutor(BaseExecutor):
+ """
+ Runs jobs in the default executor of the event loop.
+
+ If the job function is a native coroutine function, it is scheduled to be run directly in the
+ event loop as soon as possible. All other functions are run in the event loop's default
+ executor which is usually a thread pool.
+
+ Plugin alias: ``asyncio``
+ """
+
+ def start(self, scheduler, alias):
+ super(AsyncIOExecutor, self).start(scheduler, alias)
+ self._eventloop = scheduler._eventloop
+ self._pending_futures = set()
+
+ def shutdown(self, wait=True):
+ # There is no way to honor wait=True without converting this method into a coroutine method
+ for f in self._pending_futures:
+ if not f.done():
+ f.cancel()
+
+ self._pending_futures.clear()
+
+ def _do_submit_job(self, job, run_times):
+ def callback(f):
+ self._pending_futures.discard(f)
+ try:
+ events = f.result()
+ except BaseException:
+ self._run_job_error(job.id, *sys.exc_info()[1:])
+ else:
+ self._run_job_success(job.id, events)
+
+ if iscoroutinefunction_partial(job.func):
+ if run_coroutine_job is not None:
+ coro = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name)
+ f = self._eventloop.create_task(coro)
+ else:
+ raise Exception('Executing coroutine based jobs is not supported with Trollius')
+ else:
+ f = self._eventloop.run_in_executor(None, run_job, job, job._jobstore_alias, run_times,
+ self._logger.name)
+
+ f.add_done_callback(callback)
+ self._pending_futures.add(f)
diff --git a/telegramer/include/apscheduler/executors/base.py b/telegramer/include/apscheduler/executors/base.py
new file mode 100644
index 0000000..4c09fc1
--- /dev/null
+++ b/telegramer/include/apscheduler/executors/base.py
@@ -0,0 +1,146 @@
+from abc import ABCMeta, abstractmethod
+from collections import defaultdict
+from datetime import datetime, timedelta
+from traceback import format_tb
+import logging
+import sys
+
+from pytz import utc
+import six
+
+from apscheduler.events import (
+ JobExecutionEvent, EVENT_JOB_MISSED, EVENT_JOB_ERROR, EVENT_JOB_EXECUTED)
+
+
+class MaxInstancesReachedError(Exception):
+ def __init__(self, job):
+ super(MaxInstancesReachedError, self).__init__(
+ 'Job "%s" has already reached its maximum number of instances (%d)' %
+ (job.id, job.max_instances))
+
+
+class BaseExecutor(six.with_metaclass(ABCMeta, object)):
+ """Abstract base class that defines the interface that every executor must implement."""
+
+ _scheduler = None
+ _lock = None
+ _logger = logging.getLogger('apscheduler.executors')
+
+ def __init__(self):
+ super(BaseExecutor, self).__init__()
+ self._instances = defaultdict(lambda: 0)
+
+ def start(self, scheduler, alias):
+ """
+ Called by the scheduler when the scheduler is being started or when the executor is being
+ added to an already running scheduler.
+
+ :param apscheduler.schedulers.base.BaseScheduler scheduler: the scheduler that is starting
+ this executor
+ :param str|unicode alias: alias of this executor as it was assigned to the scheduler
+
+ """
+ self._scheduler = scheduler
+ self._lock = scheduler._create_lock()
+ self._logger = logging.getLogger('apscheduler.executors.%s' % alias)
+
+ def shutdown(self, wait=True):
+ """
+ Shuts down this executor.
+
+ :param bool wait: ``True`` to wait until all submitted jobs
+ have been executed
+ """
+
+ def submit_job(self, job, run_times):
+ """
+ Submits job for execution.
+
+ :param Job job: job to execute
+ :param list[datetime] run_times: list of datetimes specifying
+ when the job should have been run
+ :raises MaxInstancesReachedError: if the maximum number of
+ allowed instances for this job has been reached
+
+ """
+ assert self._lock is not None, 'This executor has not been started yet'
+ with self._lock:
+ if self._instances[job.id] >= job.max_instances:
+ raise MaxInstancesReachedError(job)
+
+ self._do_submit_job(job, run_times)
+ self._instances[job.id] += 1
+
+ @abstractmethod
+ def _do_submit_job(self, job, run_times):
+ """Performs the actual task of scheduling `run_job` to be called."""
+
+ def _run_job_success(self, job_id, events):
+ """
+ Called by the executor with the list of generated events when :func:`run_job` has been
+ successfully called.
+
+ """
+ with self._lock:
+ self._instances[job_id] -= 1
+ if self._instances[job_id] == 0:
+ del self._instances[job_id]
+
+ for event in events:
+ self._scheduler._dispatch_event(event)
+
+ def _run_job_error(self, job_id, exc, traceback=None):
+ """Called by the executor with the exception if there is an error calling `run_job`."""
+ with self._lock:
+ self._instances[job_id] -= 1
+ if self._instances[job_id] == 0:
+ del self._instances[job_id]
+
+ exc_info = (exc.__class__, exc, traceback)
+ self._logger.error('Error running job %s', job_id, exc_info=exc_info)
+
+
+def run_job(job, jobstore_alias, run_times, logger_name):
+ """
+ Called by executors to run the job. Returns a list of scheduler events to be dispatched by the
+ scheduler.
+
+ """
+ events = []
+ logger = logging.getLogger(logger_name)
+ for run_time in run_times:
+ # See if the job missed its run time window, and handle
+ # possible misfires accordingly
+ if job.misfire_grace_time is not None:
+ difference = datetime.now(utc) - run_time
+ grace_time = timedelta(seconds=job.misfire_grace_time)
+ if difference > grace_time:
+ events.append(JobExecutionEvent(EVENT_JOB_MISSED, job.id, jobstore_alias,
+ run_time))
+ logger.warning('Run time of job "%s" was missed by %s', job, difference)
+ continue
+
+ logger.info('Running job "%s" (scheduled at %s)', job, run_time)
+ try:
+ retval = job.func(*job.args, **job.kwargs)
+ except BaseException:
+ exc, tb = sys.exc_info()[1:]
+ formatted_tb = ''.join(format_tb(tb))
+ events.append(JobExecutionEvent(EVENT_JOB_ERROR, job.id, jobstore_alias, run_time,
+ exception=exc, traceback=formatted_tb))
+ logger.exception('Job "%s" raised an exception', job)
+
+ # This is to prevent cyclic references that would lead to memory leaks
+ if six.PY2:
+ sys.exc_clear()
+ del tb
+ else:
+ import traceback
+ traceback.clear_frames(tb)
+ del tb
+ else:
+ events.append(JobExecutionEvent(EVENT_JOB_EXECUTED, job.id, jobstore_alias, run_time,
+ retval=retval))
+ logger.info('Job "%s" executed successfully', job)
+
+ return events
diff --git a/telegramer/include/apscheduler/executors/base_py3.py b/telegramer/include/apscheduler/executors/base_py3.py
new file mode 100644
index 0000000..7111d2a
--- /dev/null
+++ b/telegramer/include/apscheduler/executors/base_py3.py
@@ -0,0 +1,43 @@
+import logging
+import sys
+import traceback
+from datetime import datetime, timedelta
+from traceback import format_tb
+
+from pytz import utc
+
+from apscheduler.events import (
+ JobExecutionEvent, EVENT_JOB_MISSED, EVENT_JOB_ERROR, EVENT_JOB_EXECUTED)
+
+
+async def run_coroutine_job(job, jobstore_alias, run_times, logger_name):
+ """Coroutine version of run_job()."""
+ events = []
+ logger = logging.getLogger(logger_name)
+ for run_time in run_times:
+ # See if the job missed its run time window, and handle possible misfires accordingly
+ if job.misfire_grace_time is not None:
+ difference = datetime.now(utc) - run_time
+ grace_time = timedelta(seconds=job.misfire_grace_time)
+ if difference > grace_time:
+ events.append(JobExecutionEvent(EVENT_JOB_MISSED, job.id, jobstore_alias,
+ run_time))
+ logger.warning('Run time of job "%s" was missed by %s', job, difference)
+ continue
+
+ logger.info('Running job "%s" (scheduled at %s)', job, run_time)
+ try:
+ retval = await job.func(*job.args, **job.kwargs)
+ except BaseException:
+ exc, tb = sys.exc_info()[1:]
+ formatted_tb = ''.join(format_tb(tb))
+ events.append(JobExecutionEvent(EVENT_JOB_ERROR, job.id, jobstore_alias, run_time,
+ exception=exc, traceback=formatted_tb))
+ logger.exception('Job "%s" raised an exception', job)
+ traceback.clear_frames(tb)
+ else:
+ events.append(JobExecutionEvent(EVENT_JOB_EXECUTED, job.id, jobstore_alias, run_time,
+ retval=retval))
+ logger.info('Job "%s" executed successfully', job)
+
+ return events
diff --git a/telegramer/include/apscheduler/executors/debug.py b/telegramer/include/apscheduler/executors/debug.py
new file mode 100644
index 0000000..ac739ae
--- /dev/null
+++ b/telegramer/include/apscheduler/executors/debug.py
@@ -0,0 +1,20 @@
+import sys
+
+from apscheduler.executors.base import BaseExecutor, run_job
+
+
+class DebugExecutor(BaseExecutor):
+ """
+ A special executor that executes the target callable directly instead of deferring it to a
+ thread or process.
+
+ Plugin alias: ``debug``
+ """
+
+ def _do_submit_job(self, job, run_times):
+ try:
+ events = run_job(job, job._jobstore_alias, run_times, self._logger.name)
+ except BaseException:
+ self._run_job_error(job.id, *sys.exc_info()[1:])
+ else:
+ self._run_job_success(job.id, events)
diff --git a/telegramer/include/apscheduler/executors/gevent.py b/telegramer/include/apscheduler/executors/gevent.py
new file mode 100644
index 0000000..1235bb6
--- /dev/null
+++ b/telegramer/include/apscheduler/executors/gevent.py
@@ -0,0 +1,30 @@
+from __future__ import absolute_import
+import sys
+
+from apscheduler.executors.base import BaseExecutor, run_job
+
+
+try:
+ import gevent
+except ImportError: # pragma: nocover
+ raise ImportError('GeventExecutor requires gevent installed')
+
+
+class GeventExecutor(BaseExecutor):
+ """
+ Runs jobs as greenlets.
+
+ Plugin alias: ``gevent``
+ """
+
+ def _do_submit_job(self, job, run_times):
+ def callback(greenlet):
+ try:
+ events = greenlet.get()
+ except BaseException:
+ self._run_job_error(job.id, *sys.exc_info()[1:])
+ else:
+ self._run_job_success(job.id, events)
+
+ gevent.spawn(run_job, job, job._jobstore_alias, run_times, self._logger.name).\
+ link(callback)
diff --git a/telegramer/include/apscheduler/executors/pool.py b/telegramer/include/apscheduler/executors/pool.py
new file mode 100644
index 0000000..c85896e
--- /dev/null
+++ b/telegramer/include/apscheduler/executors/pool.py
@@ -0,0 +1,71 @@
+from abc import abstractmethod
+import concurrent.futures
+
+from apscheduler.executors.base import BaseExecutor, run_job
+
+try:
+ from concurrent.futures.process import BrokenProcessPool
+except ImportError:
+ BrokenProcessPool = None
+
+
+class BasePoolExecutor(BaseExecutor):
+ @abstractmethod
+ def __init__(self, pool):
+ super(BasePoolExecutor, self).__init__()
+ self._pool = pool
+
+ def _do_submit_job(self, job, run_times):
+ def callback(f):
+ exc, tb = (f.exception_info() if hasattr(f, 'exception_info') else
+ (f.exception(), getattr(f.exception(), '__traceback__', None)))
+ if exc:
+ self._run_job_error(job.id, exc, tb)
+ else:
+ self._run_job_success(job.id, f.result())
+
+ try:
+ f = self._pool.submit(run_job, job, job._jobstore_alias, run_times, self._logger.name)
+ except BrokenProcessPool:
+ self._logger.warning('Process pool is broken; replacing pool with a fresh instance')
+ self._pool = self._pool.__class__(self._pool._max_workers)
+ f = self._pool.submit(run_job, job, job._jobstore_alias, run_times, self._logger.name)
+
+ f.add_done_callback(callback)
+
+ def shutdown(self, wait=True):
+ self._pool.shutdown(wait)
+
+
+class ThreadPoolExecutor(BasePoolExecutor):
+ """
+ An executor that runs jobs in a concurrent.futures thread pool.
+
+ Plugin alias: ``threadpool``
+
+ :param max_workers: the maximum number of spawned threads.
+ :param pool_kwargs: dict of keyword arguments to pass to the underlying
+ ThreadPoolExecutor constructor
+ """
+
+ def __init__(self, max_workers=10, pool_kwargs=None):
+ pool_kwargs = pool_kwargs or {}
+ pool = concurrent.futures.ThreadPoolExecutor(int(max_workers), **pool_kwargs)
+ super(ThreadPoolExecutor, self).__init__(pool)
+
+
+class ProcessPoolExecutor(BasePoolExecutor):
+ """
+ An executor that runs jobs in a concurrent.futures process pool.
+
+ Plugin alias: ``processpool``
+
+ :param max_workers: the maximum number of spawned processes.
+ :param pool_kwargs: dict of keyword arguments to pass to the underlying
+ ProcessPoolExecutor constructor
+ """
+
+ def __init__(self, max_workers=10, pool_kwargs=None):
+ pool_kwargs = pool_kwargs or {}
+ pool = concurrent.futures.ProcessPoolExecutor(int(max_workers), **pool_kwargs)
+ super(ProcessPoolExecutor, self).__init__(pool)
diff --git a/telegramer/include/apscheduler/executors/tornado.py b/telegramer/include/apscheduler/executors/tornado.py
new file mode 100644
index 0000000..3b97eec
--- /dev/null
+++ b/telegramer/include/apscheduler/executors/tornado.py
@@ -0,0 +1,54 @@
+from __future__ import absolute_import
+
+import sys
+from concurrent.futures import ThreadPoolExecutor
+
+from tornado.gen import convert_yielded
+
+from apscheduler.executors.base import BaseExecutor, run_job
+
+try:
+ from apscheduler.executors.base_py3 import run_coroutine_job
+ from apscheduler.util import iscoroutinefunction_partial
+except ImportError:
+ def iscoroutinefunction_partial(func):
+ return False
+
+
+class TornadoExecutor(BaseExecutor):
+ """
+ Runs jobs either in a thread pool or directly on the I/O loop.
+
+ If the job function is a native coroutine function, it is scheduled to be run directly in the
+ I/O loop as soon as possible. All other functions are run in a thread pool.
+
+ Plugin alias: ``tornado``
+
+ :param int max_workers: maximum number of worker threads in the thread pool
+ """
+
+ def __init__(self, max_workers=10):
+ super(TornadoExecutor, self).__init__()
+ self.executor = ThreadPoolExecutor(max_workers)
+
+ def start(self, scheduler, alias):
+ super(TornadoExecutor, self).start(scheduler, alias)
+ self._ioloop = scheduler._ioloop
+
+ def _do_submit_job(self, job, run_times):
+ def callback(f):
+ try:
+ events = f.result()
+ except BaseException:
+ self._run_job_error(job.id, *sys.exc_info()[1:])
+ else:
+ self._run_job_success(job.id, events)
+
+ if iscoroutinefunction_partial(job.func):
+ f = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name)
+ else:
+ f = self.executor.submit(run_job, job, job._jobstore_alias, run_times,
+ self._logger.name)
+
+ f = convert_yielded(f)
+ f.add_done_callback(callback)
diff --git a/telegramer/include/apscheduler/executors/twisted.py b/telegramer/include/apscheduler/executors/twisted.py
new file mode 100644
index 0000000..c7bcf64
--- /dev/null
+++ b/telegramer/include/apscheduler/executors/twisted.py
@@ -0,0 +1,25 @@
+from __future__ import absolute_import
+
+from apscheduler.executors.base import BaseExecutor, run_job
+
+
+class TwistedExecutor(BaseExecutor):
+ """
+ Runs jobs in the reactor's thread pool.
+
+ Plugin alias: ``twisted``
+ """
+
+ def start(self, scheduler, alias):
+ super(TwistedExecutor, self).start(scheduler, alias)
+ self._reactor = scheduler._reactor
+
+ def _do_submit_job(self, job, run_times):
+ def callback(success, result):
+ if success:
+ self._run_job_success(job.id, result)
+ else:
+ self._run_job_error(job.id, result.value, result.tb)
+
+ self._reactor.getThreadPool().callInThreadWithCallback(
+ callback, run_job, job, job._jobstore_alias, run_times, self._logger.name)
diff --git a/telegramer/include/apscheduler/job.py b/telegramer/include/apscheduler/job.py
new file mode 100644
index 0000000..445d9a8
--- /dev/null
+++ b/telegramer/include/apscheduler/job.py
@@ -0,0 +1,302 @@
+from inspect import ismethod, isclass
+from uuid import uuid4
+
+import six
+
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.util import (
+ ref_to_obj, obj_to_ref, datetime_repr, repr_escape, get_callable_name, check_callable_args,
+ convert_to_datetime)
+
+try:
+ from collections.abc import Iterable, Mapping
+except ImportError:
+ from collections import Iterable, Mapping
+
+
+class Job(object):
+ """
+ Contains the options given when scheduling callables and its current schedule and other state.
+ This class should never be instantiated by the user.
+
+ :var str id: the unique identifier of this job
+ :var str name: the description of this job
+ :var func: the callable to execute
+ :var tuple|list args: positional arguments to the callable
+ :var dict kwargs: keyword arguments to the callable
+ :var bool coalesce: whether to only run the job once when several run times are due
+ :var trigger: the trigger object that controls the schedule of this job
+ :var str executor: the name of the executor that will run this job
+ :var int misfire_grace_time: the time (in seconds) how much this job's execution is allowed to
+ be late (``None`` means "allow the job to run no matter how late it is")
+ :var int max_instances: the maximum number of concurrently executing instances allowed for this
+ job
+ :var datetime.datetime next_run_time: the next scheduled run time of this job
+
+ .. note::
+ The ``misfire_grace_time`` has some non-obvious effects on job execution. See the
+ :ref:`missed-job-executions` section in the documentation for an in-depth explanation.
+ """
+
+ __slots__ = ('_scheduler', '_jobstore_alias', 'id', 'trigger', 'executor', 'func', 'func_ref',
+ 'args', 'kwargs', 'name', 'misfire_grace_time', 'coalesce', 'max_instances',
+ 'next_run_time', '__weakref__')
+
+ def __init__(self, scheduler, id=None, **kwargs):
+ super(Job, self).__init__()
+ self._scheduler = scheduler
+ self._jobstore_alias = None
+ self._modify(id=id or uuid4().hex, **kwargs)
+
+ def modify(self, **changes):
+ """
+ Makes the given changes to this job and saves it in the associated job store.
+
+ Accepted keyword arguments are the same as the variables on this class.
+
+ .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.modify_job`
+
+ :return Job: this job instance
+
+ """
+ self._scheduler.modify_job(self.id, self._jobstore_alias, **changes)
+ return self
+
+ def reschedule(self, trigger, **trigger_args):
+ """
+ Shortcut for switching the trigger on this job.
+
+ .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.reschedule_job`
+
+ :return Job: this job instance
+
+ """
+ self._scheduler.reschedule_job(self.id, self._jobstore_alias, trigger, **trigger_args)
+ return self
+
+ def pause(self):
+ """
+ Temporarily suspend the execution of this job.
+
+ .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.pause_job`
+
+ :return Job: this job instance
+
+ """
+ self._scheduler.pause_job(self.id, self._jobstore_alias)
+ return self
+
+ def resume(self):
+ """
+ Resume the schedule of this job if previously paused.
+
+ .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.resume_job`
+
+ :return Job: this job instance
+
+ """
+ self._scheduler.resume_job(self.id, self._jobstore_alias)
+ return self
+
+ def remove(self):
+ """
+ Unschedules this job and removes it from its associated job store.
+
+ .. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.remove_job`
+
+ """
+ self._scheduler.remove_job(self.id, self._jobstore_alias)
+
+ @property
+ def pending(self):
+ """
+ Returns ``True`` if the referenced job is still waiting to be added to its designated job
+ store.
+
+ """
+ return self._jobstore_alias is None
+
+ #
+ # Private API
+ #
+
+ def _get_run_times(self, now):
+ """
+ Computes the scheduled run times between ``next_run_time`` and ``now`` (inclusive).
+
+ :type now: datetime.datetime
+ :rtype: list[datetime.datetime]
+
+ """
+ run_times = []
+ next_run_time = self.next_run_time
+ while next_run_time and next_run_time <= now:
+ run_times.append(next_run_time)
+ next_run_time = self.trigger.get_next_fire_time(next_run_time, now)
+
+ return run_times
+
+ def _modify(self, **changes):
+ """
+ Validates the changes to the Job and makes the modifications if and only if all of them
+ validate.
+
+ """
+ approved = {}
+
+ if 'id' in changes:
+ value = changes.pop('id')
+ if not isinstance(value, six.string_types):
+ raise TypeError("id must be a nonempty string")
+ if hasattr(self, 'id'):
+ raise ValueError('The job ID may not be changed')
+ approved['id'] = value
+
+ if 'func' in changes or 'args' in changes or 'kwargs' in changes:
+ func = changes.pop('func') if 'func' in changes else self.func
+ args = changes.pop('args') if 'args' in changes else self.args
+ kwargs = changes.pop('kwargs') if 'kwargs' in changes else self.kwargs
+
+ if isinstance(func, six.string_types):
+ func_ref = func
+ func = ref_to_obj(func)
+ elif callable(func):
+ try:
+ func_ref = obj_to_ref(func)
+ except ValueError:
+ # If this happens, this Job won't be serializable
+ func_ref = None
+ else:
+ raise TypeError('func must be a callable or a textual reference to one')
+
+ if not hasattr(self, 'name') and changes.get('name', None) is None:
+ changes['name'] = get_callable_name(func)
+
+ if isinstance(args, six.string_types) or not isinstance(args, Iterable):
+ raise TypeError('args must be a non-string iterable')
+ if isinstance(kwargs, six.string_types) or not isinstance(kwargs, Mapping):
+ raise TypeError('kwargs must be a dict-like object')
+
+ check_callable_args(func, args, kwargs)
+
+ approved['func'] = func
+ approved['func_ref'] = func_ref
+ approved['args'] = args
+ approved['kwargs'] = kwargs
+
+ if 'name' in changes:
+ value = changes.pop('name')
+ if not value or not isinstance(value, six.string_types):
+ raise TypeError("name must be a nonempty string")
+ approved['name'] = value
+
+ if 'misfire_grace_time' in changes:
+ value = changes.pop('misfire_grace_time')
+ if value is not None and (not isinstance(value, six.integer_types) or value <= 0):
+ raise TypeError('misfire_grace_time must be either None or a positive integer')
+ approved['misfire_grace_time'] = value
+
+ if 'coalesce' in changes:
+ value = bool(changes.pop('coalesce'))
+ approved['coalesce'] = value
+
+ if 'max_instances' in changes:
+ value = changes.pop('max_instances')
+ if not isinstance(value, six.integer_types) or value <= 0:
+ raise TypeError('max_instances must be a positive integer')
+ approved['max_instances'] = value
+
+ if 'trigger' in changes:
+ trigger = changes.pop('trigger')
+ if not isinstance(trigger, BaseTrigger):
+ raise TypeError('Expected a trigger instance, got %s instead' %
+ trigger.__class__.__name__)
+
+ approved['trigger'] = trigger
+
+ if 'executor' in changes:
+ value = changes.pop('executor')
+ if not isinstance(value, six.string_types):
+ raise TypeError('executor must be a string')
+ approved['executor'] = value
+
+ if 'next_run_time' in changes:
+ value = changes.pop('next_run_time')
+ approved['next_run_time'] = convert_to_datetime(value, self._scheduler.timezone,
+ 'next_run_time')
+
+ if changes:
+ raise AttributeError('The following are not modifiable attributes of Job: %s' %
+ ', '.join(changes))
+
+ for key, value in six.iteritems(approved):
+ setattr(self, key, value)
+
+ def __getstate__(self):
+ # Don't allow this Job to be serialized if the function reference could not be determined
+ if not self.func_ref:
+ raise ValueError(
+ 'This Job cannot be serialized since the reference to its callable (%r) could not '
+ 'be determined. Consider giving a textual reference (module:function name) '
+ 'instead.' % (self.func,))
+
+ # Instance methods cannot survive serialization as-is, so store the "self" argument
+ # explicitly
+ func = self.func
+ if ismethod(func) and not isclass(func.__self__) and obj_to_ref(func) == self.func_ref:
+ args = (func.__self__,) + tuple(self.args)
+ else:
+ args = self.args
+
+ return {
+ 'version': 1,
+ 'id': self.id,
+ 'func': self.func_ref,
+ 'trigger': self.trigger,
+ 'executor': self.executor,
+ 'args': args,
+ 'kwargs': self.kwargs,
+ 'name': self.name,
+ 'misfire_grace_time': self.misfire_grace_time,
+ 'coalesce': self.coalesce,
+ 'max_instances': self.max_instances,
+ 'next_run_time': self.next_run_time
+ }
+
+ def __setstate__(self, state):
+ if state.get('version', 1) > 1:
+ raise ValueError('Job has version %s, but only version 1 can be handled' %
+ state['version'])
+
+ self.id = state['id']
+ self.func_ref = state['func']
+ self.func = ref_to_obj(self.func_ref)
+ self.trigger = state['trigger']
+ self.executor = state['executor']
+ self.args = state['args']
+ self.kwargs = state['kwargs']
+ self.name = state['name']
+ self.misfire_grace_time = state['misfire_grace_time']
+ self.coalesce = state['coalesce']
+ self.max_instances = state['max_instances']
+ self.next_run_time = state['next_run_time']
+
+ def __eq__(self, other):
+ if isinstance(other, Job):
+ return self.id == other.id
+ return NotImplemented
+
+ def __repr__(self):
+ return '' % (repr_escape(self.id), repr_escape(self.name))
+
+ def __str__(self):
+ return repr_escape(self.__unicode__())
+
+ def __unicode__(self):
+ if hasattr(self, 'next_run_time'):
+ status = ('next run at: ' + datetime_repr(self.next_run_time) if
+ self.next_run_time else 'paused')
+ else:
+ status = 'pending'
+
+ return u'%s (trigger: %s, %s)' % (self.name, self.trigger, status)
diff --git a/telegramer/include/apscheduler/jobstores/__init__.py b/telegramer/include/apscheduler/jobstores/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/telegramer/include/apscheduler/jobstores/base.py b/telegramer/include/apscheduler/jobstores/base.py
new file mode 100644
index 0000000..9cff66c
--- /dev/null
+++ b/telegramer/include/apscheduler/jobstores/base.py
@@ -0,0 +1,143 @@
+from abc import ABCMeta, abstractmethod
+import logging
+
+import six
+
+
+class JobLookupError(KeyError):
+ """Raised when the job store cannot find a job for update or removal."""
+
+ def __init__(self, job_id):
+ super(JobLookupError, self).__init__(u'No job by the id of %s was found' % job_id)
+
+
+class ConflictingIdError(KeyError):
+ """Raised when the uniqueness of job IDs is being violated."""
+
+ def __init__(self, job_id):
+ super(ConflictingIdError, self).__init__(
+ u'Job identifier (%s) conflicts with an existing job' % job_id)
+
+
+class TransientJobError(ValueError):
+ """
+ Raised when an attempt to add transient (with no func_ref) job to a persistent job store is
+ detected.
+ """
+
+ def __init__(self, job_id):
+ super(TransientJobError, self).__init__(
+ u'Job (%s) cannot be added to this job store because a reference to the callable '
+ u'could not be determined.' % job_id)
+
+
+class BaseJobStore(six.with_metaclass(ABCMeta)):
+ """Abstract base class that defines the interface that every job store must implement."""
+
+ _scheduler = None
+ _alias = None
+ _logger = logging.getLogger('apscheduler.jobstores')
+
+ def start(self, scheduler, alias):
+ """
+ Called by the scheduler when the scheduler is being started or when the job store is being
+ added to an already running scheduler.
+
+ :param apscheduler.schedulers.base.BaseScheduler scheduler: the scheduler that is starting
+ this job store
+ :param str|unicode alias: alias of this job store as it was assigned to the scheduler
+ """
+
+ self._scheduler = scheduler
+ self._alias = alias
+ self._logger = logging.getLogger('apscheduler.jobstores.%s' % alias)
+
+ def shutdown(self):
+ """Frees any resources still bound to this job store."""
+
+ def _fix_paused_jobs_sorting(self, jobs):
+ for i, job in enumerate(jobs):
+ if job.next_run_time is not None:
+ if i > 0:
+ paused_jobs = jobs[:i]
+ del jobs[:i]
+ jobs.extend(paused_jobs)
+ break
+
+ @abstractmethod
+ def lookup_job(self, job_id):
+ """
+ Returns a specific job, or ``None`` if it isn't found..
+
+ The job store is responsible for setting the ``scheduler`` and ``jobstore`` attributes of
+ the returned job to point to the scheduler and itself, respectively.
+
+ :param str|unicode job_id: identifier of the job
+ :rtype: Job
+ """
+
+ @abstractmethod
+ def get_due_jobs(self, now):
+ """
+ Returns the list of jobs that have ``next_run_time`` earlier or equal to ``now``.
+ The returned jobs must be sorted by next run time (ascending).
+
+ :param datetime.datetime now: the current (timezone aware) datetime
+ :rtype: list[Job]
+ """
+
+ @abstractmethod
+ def get_next_run_time(self):
+ """
+ Returns the earliest run time of all the jobs stored in this job store, or ``None`` if
+ there are no active jobs.
+
+ :rtype: datetime.datetime
+ """
+
+ @abstractmethod
+ def get_all_jobs(self):
+ """
+ Returns a list of all jobs in this job store.
+ The returned jobs should be sorted by next run time (ascending).
+ Paused jobs (next_run_time == None) should be sorted last.
+
+ The job store is responsible for setting the ``scheduler`` and ``jobstore`` attributes of
+ the returned jobs to point to the scheduler and itself, respectively.
+
+ :rtype: list[Job]
+ """
+
+ @abstractmethod
+ def add_job(self, job):
+ """
+ Adds the given job to this store.
+
+ :param Job job: the job to add
+ :raises ConflictingIdError: if there is another job in this store with the same ID
+ """
+
+ @abstractmethod
+ def update_job(self, job):
+ """
+ Replaces the job in the store with the given newer version.
+
+ :param Job job: the job to update
+ :raises JobLookupError: if the job does not exist
+ """
+
+ @abstractmethod
+ def remove_job(self, job_id):
+ """
+ Removes the given job from this store.
+
+ :param str|unicode job_id: identifier of the job
+ :raises JobLookupError: if the job does not exist
+ """
+
+ @abstractmethod
+ def remove_all_jobs(self):
+ """Removes all jobs from this store."""
+
+ def __repr__(self):
+ return '<%s>' % self.__class__.__name__
diff --git a/telegramer/include/apscheduler/jobstores/memory.py b/telegramer/include/apscheduler/jobstores/memory.py
new file mode 100644
index 0000000..abfe7c6
--- /dev/null
+++ b/telegramer/include/apscheduler/jobstores/memory.py
@@ -0,0 +1,108 @@
+from __future__ import absolute_import
+
+from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
+from apscheduler.util import datetime_to_utc_timestamp
+
+
+class MemoryJobStore(BaseJobStore):
+ """
+ Stores jobs in an array in RAM. Provides no persistence support.
+
+ Plugin alias: ``memory``
+ """
+
+ def __init__(self):
+ super(MemoryJobStore, self).__init__()
+ # list of (job, timestamp), sorted by next_run_time and job id (ascending)
+ self._jobs = []
+ self._jobs_index = {} # id -> (job, timestamp) lookup table
+
+ def lookup_job(self, job_id):
+ return self._jobs_index.get(job_id, (None, None))[0]
+
+ def get_due_jobs(self, now):
+ now_timestamp = datetime_to_utc_timestamp(now)
+ pending = []
+ for job, timestamp in self._jobs:
+ if timestamp is None or timestamp > now_timestamp:
+ break
+ pending.append(job)
+
+ return pending
+
+ def get_next_run_time(self):
+ return self._jobs[0][0].next_run_time if self._jobs else None
+
+ def get_all_jobs(self):
+ return [j[0] for j in self._jobs]
+
+ def add_job(self, job):
+ if job.id in self._jobs_index:
+ raise ConflictingIdError(job.id)
+
+ timestamp = datetime_to_utc_timestamp(job.next_run_time)
+ index = self._get_job_index(timestamp, job.id)
+ self._jobs.insert(index, (job, timestamp))
+ self._jobs_index[job.id] = (job, timestamp)
+
+ def update_job(self, job):
+ old_job, old_timestamp = self._jobs_index.get(job.id, (None, None))
+ if old_job is None:
+ raise JobLookupError(job.id)
+
+ # If the next run time has not changed, simply replace the job in its present index.
+ # Otherwise, reinsert the job to the list to preserve the ordering.
+ old_index = self._get_job_index(old_timestamp, old_job.id)
+ new_timestamp = datetime_to_utc_timestamp(job.next_run_time)
+ if old_timestamp == new_timestamp:
+ self._jobs[old_index] = (job, new_timestamp)
+ else:
+ del self._jobs[old_index]
+ new_index = self._get_job_index(new_timestamp, job.id)
+ self._jobs.insert(new_index, (job, new_timestamp))
+
+ self._jobs_index[old_job.id] = (job, new_timestamp)
+
+ def remove_job(self, job_id):
+ job, timestamp = self._jobs_index.get(job_id, (None, None))
+ if job is None:
+ raise JobLookupError(job_id)
+
+ index = self._get_job_index(timestamp, job_id)
+ del self._jobs[index]
+ del self._jobs_index[job.id]
+
+ def remove_all_jobs(self):
+ self._jobs = []
+ self._jobs_index = {}
+
+ def shutdown(self):
+ self.remove_all_jobs()
+
+ def _get_job_index(self, timestamp, job_id):
+ """
+ Returns the index of the given job, or if it's not found, the index where the job should be
+ inserted based on the given timestamp.
+
+ :type timestamp: int
+ :type job_id: str
+
+ """
+ lo, hi = 0, len(self._jobs)
+ timestamp = float('inf') if timestamp is None else timestamp
+ while lo < hi:
+ mid = (lo + hi) // 2
+ mid_job, mid_timestamp = self._jobs[mid]
+ mid_timestamp = float('inf') if mid_timestamp is None else mid_timestamp
+ if mid_timestamp > timestamp:
+ hi = mid
+ elif mid_timestamp < timestamp:
+ lo = mid + 1
+ elif mid_job.id > job_id:
+ hi = mid
+ elif mid_job.id < job_id:
+ lo = mid + 1
+ else:
+ return mid
+
+ return lo
diff --git a/telegramer/include/apscheduler/jobstores/mongodb.py b/telegramer/include/apscheduler/jobstores/mongodb.py
new file mode 100644
index 0000000..5a00f94
--- /dev/null
+++ b/telegramer/include/apscheduler/jobstores/mongodb.py
@@ -0,0 +1,141 @@
+from __future__ import absolute_import
+import warnings
+
+from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
+from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
+from apscheduler.job import Job
+
+try:
+ import cPickle as pickle
+except ImportError: # pragma: nocover
+ import pickle
+
+try:
+ from bson.binary import Binary
+ from pymongo.errors import DuplicateKeyError
+ from pymongo import MongoClient, ASCENDING
+except ImportError: # pragma: nocover
+ raise ImportError('MongoDBJobStore requires PyMongo installed')
+
+
+class MongoDBJobStore(BaseJobStore):
+ """
+ Stores jobs in a MongoDB database. Any leftover keyword arguments are directly passed to
+ pymongo's `MongoClient
+ `_.
+
+ Plugin alias: ``mongodb``
+
+ :param str database: database to store jobs in
+ :param str collection: collection to store jobs in
+ :param client: a :class:`~pymongo.mongo_client.MongoClient` instance to use instead of
+ providing connection arguments
+ :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
+ highest available
+ """
+
+ def __init__(self, database='apscheduler', collection='jobs', client=None,
+ pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
+ super(MongoDBJobStore, self).__init__()
+ self.pickle_protocol = pickle_protocol
+
+ if not database:
+ raise ValueError('The "database" parameter must not be empty')
+ if not collection:
+ raise ValueError('The "collection" parameter must not be empty')
+
+ if client:
+ self.client = maybe_ref(client)
+ else:
+ connect_args.setdefault('w', 1)
+ self.client = MongoClient(**connect_args)
+
+ self.collection = self.client[database][collection]
+
+ def start(self, scheduler, alias):
+ super(MongoDBJobStore, self).start(scheduler, alias)
+ self.collection.create_index('next_run_time', sparse=True)
+
+ @property
+ def connection(self):
+ warnings.warn('The "connection" member is deprecated -- use "client" instead',
+ DeprecationWarning)
+ return self.client
+
+ def lookup_job(self, job_id):
+ document = self.collection.find_one(job_id, ['job_state'])
+ return self._reconstitute_job(document['job_state']) if document else None
+
+ def get_due_jobs(self, now):
+ timestamp = datetime_to_utc_timestamp(now)
+ return self._get_jobs({'next_run_time': {'$lte': timestamp}})
+
+ def get_next_run_time(self):
+ document = self.collection.find_one({'next_run_time': {'$ne': None}},
+ projection=['next_run_time'],
+ sort=[('next_run_time', ASCENDING)])
+ return utc_timestamp_to_datetime(document['next_run_time']) if document else None
+
+ def get_all_jobs(self):
+ jobs = self._get_jobs({})
+ self._fix_paused_jobs_sorting(jobs)
+ return jobs
+
+ def add_job(self, job):
+ try:
+ self.collection.insert_one({
+ '_id': job.id,
+ 'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
+ 'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
+ })
+ except DuplicateKeyError:
+ raise ConflictingIdError(job.id)
+
+ def update_job(self, job):
+ changes = {
+ 'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
+ 'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
+ }
+ result = self.collection.update_one({'_id': job.id}, {'$set': changes})
+ if result and result.matched_count == 0:
+ raise JobLookupError(job.id)
+
+ def remove_job(self, job_id):
+ result = self.collection.delete_one({'_id': job_id})
+ if result and result.deleted_count == 0:
+ raise JobLookupError(job_id)
+
+ def remove_all_jobs(self):
+ self.collection.delete_many({})
+
+ def shutdown(self):
+ self.client.close()
+
+ def _reconstitute_job(self, job_state):
+ job_state = pickle.loads(job_state)
+ job = Job.__new__(Job)
+ job.__setstate__(job_state)
+ job._scheduler = self._scheduler
+ job._jobstore_alias = self._alias
+ return job
+
+ def _get_jobs(self, conditions):
+ jobs = []
+ failed_job_ids = []
+ for document in self.collection.find(conditions, ['_id', 'job_state'],
+ sort=[('next_run_time', ASCENDING)]):
+ try:
+ jobs.append(self._reconstitute_job(document['job_state']))
+ except BaseException:
+ self._logger.exception('Unable to restore job "%s" -- removing it',
+ document['_id'])
+ failed_job_ids.append(document['_id'])
+
+ # Remove all the jobs we failed to restore
+ if failed_job_ids:
+ self.collection.delete_many({'_id': {'$in': failed_job_ids}})
+
+ return jobs
+
+ def __repr__(self):
+ return '<%s (client=%s)>' % (self.__class__.__name__, self.client)
diff --git a/telegramer/include/apscheduler/jobstores/redis.py b/telegramer/include/apscheduler/jobstores/redis.py
new file mode 100644
index 0000000..5bb69d6
--- /dev/null
+++ b/telegramer/include/apscheduler/jobstores/redis.py
@@ -0,0 +1,150 @@
+from __future__ import absolute_import
+from datetime import datetime
+
+from pytz import utc
+import six
+
+from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
+from apscheduler.util import datetime_to_utc_timestamp, utc_timestamp_to_datetime
+from apscheduler.job import Job
+
+try:
+ import cPickle as pickle
+except ImportError: # pragma: nocover
+ import pickle
+
+try:
+ from redis import Redis
+except ImportError: # pragma: nocover
+ raise ImportError('RedisJobStore requires redis installed')
+
+
+class RedisJobStore(BaseJobStore):
+ """
+ Stores jobs in a Redis database. Any leftover keyword arguments are directly passed to redis's
+ :class:`~redis.StrictRedis`.
+
+ Plugin alias: ``redis``
+
+ :param int db: the database number to store jobs in
+ :param str jobs_key: key to store jobs in
+ :param str run_times_key: key to store the jobs' run times in
+ :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
+ highest available
+ """
+
+ def __init__(self, db=0, jobs_key='apscheduler.jobs', run_times_key='apscheduler.run_times',
+ pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
+ super(RedisJobStore, self).__init__()
+
+ if db is None:
+ raise ValueError('The "db" parameter must not be empty')
+ if not jobs_key:
+ raise ValueError('The "jobs_key" parameter must not be empty')
+ if not run_times_key:
+ raise ValueError('The "run_times_key" parameter must not be empty')
+
+ self.pickle_protocol = pickle_protocol
+ self.jobs_key = jobs_key
+ self.run_times_key = run_times_key
+ self.redis = Redis(db=int(db), **connect_args)
+
+ def lookup_job(self, job_id):
+ job_state = self.redis.hget(self.jobs_key, job_id)
+ return self._reconstitute_job(job_state) if job_state else None
+
+ def get_due_jobs(self, now):
+ timestamp = datetime_to_utc_timestamp(now)
+ job_ids = self.redis.zrangebyscore(self.run_times_key, 0, timestamp)
+ if job_ids:
+ job_states = self.redis.hmget(self.jobs_key, *job_ids)
+ return self._reconstitute_jobs(six.moves.zip(job_ids, job_states))
+ return []
+
+ def get_next_run_time(self):
+ next_run_time = self.redis.zrange(self.run_times_key, 0, 0, withscores=True)
+ if next_run_time:
+ return utc_timestamp_to_datetime(next_run_time[0][1])
+
+ def get_all_jobs(self):
+ job_states = self.redis.hgetall(self.jobs_key)
+ jobs = self._reconstitute_jobs(six.iteritems(job_states))
+ paused_sort_key = datetime(9999, 12, 31, tzinfo=utc)
+ return sorted(jobs, key=lambda job: job.next_run_time or paused_sort_key)
+
+ def add_job(self, job):
+ if self.redis.hexists(self.jobs_key, job.id):
+ raise ConflictingIdError(job.id)
+
+ with self.redis.pipeline() as pipe:
+ pipe.multi()
+ pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(),
+ self.pickle_protocol))
+ if job.next_run_time:
+ pipe.zadd(self.run_times_key,
+ {job.id: datetime_to_utc_timestamp(job.next_run_time)})
+
+ pipe.execute()
+
+ def update_job(self, job):
+ if not self.redis.hexists(self.jobs_key, job.id):
+ raise JobLookupError(job.id)
+
+ with self.redis.pipeline() as pipe:
+ pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(),
+ self.pickle_protocol))
+ if job.next_run_time:
+ pipe.zadd(self.run_times_key,
+ {job.id: datetime_to_utc_timestamp(job.next_run_time)})
+ else:
+ pipe.zrem(self.run_times_key, job.id)
+
+ pipe.execute()
+
+ def remove_job(self, job_id):
+ if not self.redis.hexists(self.jobs_key, job_id):
+ raise JobLookupError(job_id)
+
+ with self.redis.pipeline() as pipe:
+ pipe.hdel(self.jobs_key, job_id)
+ pipe.zrem(self.run_times_key, job_id)
+ pipe.execute()
+
+ def remove_all_jobs(self):
+ with self.redis.pipeline() as pipe:
+ pipe.delete(self.jobs_key)
+ pipe.delete(self.run_times_key)
+ pipe.execute()
+
+ def shutdown(self):
+ self.redis.connection_pool.disconnect()
+
+ def _reconstitute_job(self, job_state):
+ job_state = pickle.loads(job_state)
+ job = Job.__new__(Job)
+ job.__setstate__(job_state)
+ job._scheduler = self._scheduler
+ job._jobstore_alias = self._alias
+ return job
+
+ def _reconstitute_jobs(self, job_states):
+ jobs = []
+ failed_job_ids = []
+ for job_id, job_state in job_states:
+ try:
+ jobs.append(self._reconstitute_job(job_state))
+ except BaseException:
+ self._logger.exception('Unable to restore job "%s" -- removing it', job_id)
+ failed_job_ids.append(job_id)
+
+ # Remove all the jobs we failed to restore
+ if failed_job_ids:
+ with self.redis.pipeline() as pipe:
+ pipe.hdel(self.jobs_key, *failed_job_ids)
+ pipe.zrem(self.run_times_key, *failed_job_ids)
+ pipe.execute()
+
+ return jobs
+
+ def __repr__(self):
+ return '<%s>' % self.__class__.__name__
diff --git a/telegramer/include/apscheduler/jobstores/rethinkdb.py b/telegramer/include/apscheduler/jobstores/rethinkdb.py
new file mode 100644
index 0000000..d8a78cd
--- /dev/null
+++ b/telegramer/include/apscheduler/jobstores/rethinkdb.py
@@ -0,0 +1,155 @@
+from __future__ import absolute_import
+
+from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
+from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
+from apscheduler.job import Job
+
+try:
+ import cPickle as pickle
+except ImportError: # pragma: nocover
+ import pickle
+
+try:
+ from rethinkdb import RethinkDB
+except ImportError: # pragma: nocover
+ raise ImportError('RethinkDBJobStore requires rethinkdb installed')
+
+
+class RethinkDBJobStore(BaseJobStore):
+ """
+ Stores jobs in a RethinkDB database. Any leftover keyword arguments are directly passed to
+ rethinkdb's `RethinkdbClient `_.
+
+ Plugin alias: ``rethinkdb``
+
+ :param str database: database to store jobs in
+ :param str collection: collection to store jobs in
+ :param client: a :class:`rethinkdb.net.Connection` instance to use instead of providing
+ connection arguments
+ :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
+ highest available
+ """
+
+ def __init__(self, database='apscheduler', table='jobs', client=None,
+ pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
+ super(RethinkDBJobStore, self).__init__()
+
+ if not database:
+ raise ValueError('The "database" parameter must not be empty')
+ if not table:
+ raise ValueError('The "table" parameter must not be empty')
+
+ self.database = database
+ self.table_name = table
+ self.table = None
+ self.client = client
+ self.pickle_protocol = pickle_protocol
+ self.connect_args = connect_args
+ self.r = RethinkDB()
+ self.conn = None
+
+ def start(self, scheduler, alias):
+ super(RethinkDBJobStore, self).start(scheduler, alias)
+
+ if self.client:
+ self.conn = maybe_ref(self.client)
+ else:
+ self.conn = self.r.connect(db=self.database, **self.connect_args)
+
+ if self.database not in self.r.db_list().run(self.conn):
+ self.r.db_create(self.database).run(self.conn)
+
+ if self.table_name not in self.r.table_list().run(self.conn):
+ self.r.table_create(self.table_name).run(self.conn)
+
+ if 'next_run_time' not in self.r.table(self.table_name).index_list().run(self.conn):
+ self.r.table(self.table_name).index_create('next_run_time').run(self.conn)
+
+ self.table = self.r.db(self.database).table(self.table_name)
+
+ def lookup_job(self, job_id):
+ results = list(self.table.get_all(job_id).pluck('job_state').run(self.conn))
+ return self._reconstitute_job(results[0]['job_state']) if results else None
+
+ def get_due_jobs(self, now):
+ return self._get_jobs(self.r.row['next_run_time'] <= datetime_to_utc_timestamp(now))
+
+ def get_next_run_time(self):
+ results = list(
+ self.table
+ .filter(self.r.row['next_run_time'] != None) # noqa
+ .order_by(self.r.asc('next_run_time'))
+ .map(lambda x: x['next_run_time'])
+ .limit(1)
+ .run(self.conn)
+ )
+ return utc_timestamp_to_datetime(results[0]) if results else None
+
+ def get_all_jobs(self):
+ jobs = self._get_jobs()
+ self._fix_paused_jobs_sorting(jobs)
+ return jobs
+
+ def add_job(self, job):
+ job_dict = {
+ 'id': job.id,
+ 'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
+ 'job_state': self.r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
+ }
+ results = self.table.insert(job_dict).run(self.conn)
+ if results['errors'] > 0:
+ raise ConflictingIdError(job.id)
+
+ def update_job(self, job):
+ changes = {
+ 'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
+ 'job_state': self.r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
+ }
+ results = self.table.get_all(job.id).update(changes).run(self.conn)
+ skipped = False in map(lambda x: results[x] == 0, results.keys())
+ if results['skipped'] > 0 or results['errors'] > 0 or not skipped:
+ raise JobLookupError(job.id)
+
+ def remove_job(self, job_id):
+ results = self.table.get_all(job_id).delete().run(self.conn)
+ if results['deleted'] + results['skipped'] != 1:
+ raise JobLookupError(job_id)
+
+ def remove_all_jobs(self):
+ self.table.delete().run(self.conn)
+
+ def shutdown(self):
+ self.conn.close()
+
+ def _reconstitute_job(self, job_state):
+ job_state = pickle.loads(job_state)
+ job = Job.__new__(Job)
+ job.__setstate__(job_state)
+ job._scheduler = self._scheduler
+ job._jobstore_alias = self._alias
+ return job
+
+ def _get_jobs(self, predicate=None):
+ jobs = []
+ failed_job_ids = []
+ query = (self.table.filter(self.r.row['next_run_time'] != None).filter(predicate) # noqa
+ if predicate else self.table)
+ query = query.order_by('next_run_time', 'id').pluck('id', 'job_state')
+
+ for document in query.run(self.conn):
+ try:
+ jobs.append(self._reconstitute_job(document['job_state']))
+ except Exception:
+ self._logger.exception('Unable to restore job "%s" -- removing it', document['id'])
+ failed_job_ids.append(document['id'])
+
+ # Remove all the jobs we failed to restore
+ if failed_job_ids:
+ self.r.expr(failed_job_ids).for_each(
+ lambda job_id: self.table.get_all(job_id).delete()).run(self.conn)
+
+ return jobs
+
+ def __repr__(self):
+ connection = self.conn
+ return '<%s (connection=%s)>' % (self.__class__.__name__, connection)
diff --git a/telegramer/include/apscheduler/jobstores/sqlalchemy.py b/telegramer/include/apscheduler/jobstores/sqlalchemy.py
new file mode 100644
index 0000000..dcfd3e5
--- /dev/null
+++ b/telegramer/include/apscheduler/jobstores/sqlalchemy.py
@@ -0,0 +1,154 @@
+from __future__ import absolute_import
+
+from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
+from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
+from apscheduler.job import Job
+
+try:
+ import cPickle as pickle
+except ImportError: # pragma: nocover
+ import pickle
+
+try:
+ from sqlalchemy import (
+ create_engine, Table, Column, MetaData, Unicode, Float, LargeBinary, select, and_)
+ from sqlalchemy.exc import IntegrityError
+ from sqlalchemy.sql.expression import null
+except ImportError: # pragma: nocover
+ raise ImportError('SQLAlchemyJobStore requires SQLAlchemy installed')
+
+
+class SQLAlchemyJobStore(BaseJobStore):
+ """
+ Stores jobs in a database table using SQLAlchemy.
+ The table will be created if it doesn't exist in the database.
+
+ Plugin alias: ``sqlalchemy``
+
+ :param str url: connection string (see
+ :ref:`SQLAlchemy documentation ` on this)
+ :param engine: an SQLAlchemy :class:`~sqlalchemy.engine.Engine` to use instead of creating a
+ new one based on ``url``
+ :param str tablename: name of the table to store jobs in
+ :param metadata: a :class:`~sqlalchemy.schema.MetaData` instance to use instead of creating a
+ new one
+ :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
+ highest available
+ :param str tableschema: name of the (existing) schema in the target database where the table
+ should be
+ :param dict engine_options: keyword arguments to :func:`~sqlalchemy.create_engine`
+ (ignored if ``engine`` is given)
+ """
+
+ def __init__(self, url=None, engine=None, tablename='apscheduler_jobs', metadata=None,
+ pickle_protocol=pickle.HIGHEST_PROTOCOL, tableschema=None, engine_options=None):
+ super(SQLAlchemyJobStore, self).__init__()
+ self.pickle_protocol = pickle_protocol
+ metadata = maybe_ref(metadata) or MetaData()
+
+ if engine:
+ self.engine = maybe_ref(engine)
+ elif url:
+ self.engine = create_engine(url, **(engine_options or {}))
+ else:
+ raise ValueError('Need either "engine" or "url" defined')
+
+ # 191 = max key length in MySQL for InnoDB/utf8mb4 tables,
+ # 25 = precision that translates to an 8-byte float
+ self.jobs_t = Table(
+ tablename, metadata,
+ Column('id', Unicode(191, _warn_on_bytestring=False), primary_key=True),
+ Column('next_run_time', Float(25), index=True),
+ Column('job_state', LargeBinary, nullable=False),
+ schema=tableschema
+ )
+
+ def start(self, scheduler, alias):
+ super(SQLAlchemyJobStore, self).start(scheduler, alias)
+ self.jobs_t.create(self.engine, True)
+
+ def lookup_job(self, job_id):
+ selectable = select([self.jobs_t.c.job_state]).where(self.jobs_t.c.id == job_id)
+ job_state = self.engine.execute(selectable).scalar()
+ return self._reconstitute_job(job_state) if job_state else None
+
+ def get_due_jobs(self, now):
+ timestamp = datetime_to_utc_timestamp(now)
+ return self._get_jobs(self.jobs_t.c.next_run_time <= timestamp)
+
+ def get_next_run_time(self):
+ selectable = select([self.jobs_t.c.next_run_time]).\
+ where(self.jobs_t.c.next_run_time != null()).\
+ order_by(self.jobs_t.c.next_run_time).limit(1)
+ next_run_time = self.engine.execute(selectable).scalar()
+ return utc_timestamp_to_datetime(next_run_time)
+
+ def get_all_jobs(self):
+ jobs = self._get_jobs()
+ self._fix_paused_jobs_sorting(jobs)
+ return jobs
+
+ def add_job(self, job):
+ insert = self.jobs_t.insert().values(**{
+ 'id': job.id,
+ 'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
+ 'job_state': pickle.dumps(job.__getstate__(), self.pickle_protocol)
+ })
+ try:
+ self.engine.execute(insert)
+ except IntegrityError:
+ raise ConflictingIdError(job.id)
+
+ def update_job(self, job):
+ update = self.jobs_t.update().values(**{
+ 'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
+ 'job_state': pickle.dumps(job.__getstate__(), self.pickle_protocol)
+ }).where(self.jobs_t.c.id == job.id)
+ result = self.engine.execute(update)
+ if result.rowcount == 0:
+ raise JobLookupError(job.id)
+
+ def remove_job(self, job_id):
+ delete = self.jobs_t.delete().where(self.jobs_t.c.id == job_id)
+ result = self.engine.execute(delete)
+ if result.rowcount == 0:
+ raise JobLookupError(job_id)
+
+ def remove_all_jobs(self):
+ delete = self.jobs_t.delete()
+ self.engine.execute(delete)
+
+ def shutdown(self):
+ self.engine.dispose()
+
+ def _reconstitute_job(self, job_state):
+ job_state = pickle.loads(job_state)
+ job_state['jobstore'] = self
+ job = Job.__new__(Job)
+ job.__setstate__(job_state)
+ job._scheduler = self._scheduler
+ job._jobstore_alias = self._alias
+ return job
+
+ def _get_jobs(self, *conditions):
+ jobs = []
+ selectable = select([self.jobs_t.c.id, self.jobs_t.c.job_state]).\
+ order_by(self.jobs_t.c.next_run_time)
+ selectable = selectable.where(and_(*conditions)) if conditions else selectable
+ failed_job_ids = set()
+ for row in self.engine.execute(selectable):
+ try:
+ jobs.append(self._reconstitute_job(row.job_state))
+ except BaseException:
+ self._logger.exception('Unable to restore job "%s" -- removing it', row.id)
+ failed_job_ids.add(row.id)
+
+ # Remove all the jobs we failed to restore
+ if failed_job_ids:
+ delete = self.jobs_t.delete().where(self.jobs_t.c.id.in_(failed_job_ids))
+ self.engine.execute(delete)
+
+ return jobs
+
+ def __repr__(self):
+ return '<%s (url=%s)>' % (self.__class__.__name__, self.engine.url)
diff --git a/telegramer/include/apscheduler/jobstores/zookeeper.py b/telegramer/include/apscheduler/jobstores/zookeeper.py
new file mode 100644
index 0000000..5253069
--- /dev/null
+++ b/telegramer/include/apscheduler/jobstores/zookeeper.py
@@ -0,0 +1,178 @@
+from __future__ import absolute_import
+
+from datetime import datetime
+
+from pytz import utc
+from kazoo.exceptions import NoNodeError, NodeExistsError
+
+from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
+from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
+from apscheduler.job import Job
+
+try:
+ import cPickle as pickle
+except ImportError: # pragma: nocover
+ import pickle
+
+try:
+ from kazoo.client import KazooClient
+except ImportError: # pragma: nocover
+ raise ImportError('ZooKeeperJobStore requires Kazoo installed')
+
+
+class ZooKeeperJobStore(BaseJobStore):
+ """
+ Stores jobs in a ZooKeeper tree. Any leftover keyword arguments are directly passed to
+ kazoo's `KazooClient
+ `_.
+
+ Plugin alias: ``zookeeper``
+
+ :param str path: path to store jobs in
+ :param client: a :class:`~kazoo.client.KazooClient` instance to use instead of
+ providing connection arguments
+ :param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
+ highest available
+ """
+
+ def __init__(self, path='/apscheduler', client=None, close_connection_on_exit=False,
+ pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
+ super(ZooKeeperJobStore, self).__init__()
+ self.pickle_protocol = pickle_protocol
+ self.close_connection_on_exit = close_connection_on_exit
+
+ if not path:
+ raise ValueError('The "path" parameter must not be empty')
+
+ self.path = path
+
+ if client:
+ self.client = maybe_ref(client)
+ else:
+ self.client = KazooClient(**connect_args)
+ self._ensured_path = False
+
+ def _ensure_paths(self):
+ if not self._ensured_path:
+ self.client.ensure_path(self.path)
+ self._ensured_path = True
+
+ def start(self, scheduler, alias):
+ super(ZooKeeperJobStore, self).start(scheduler, alias)
+ if not self.client.connected:
+ self.client.start()
+
+ def lookup_job(self, job_id):
+ self._ensure_paths()
+ node_path = self.path + "/" + str(job_id)
+ try:
+ content, _ = self.client.get(node_path)
+ doc = pickle.loads(content)
+ job = self._reconstitute_job(doc['job_state'])
+ return job
+ except BaseException:
+ return None
+
+ def get_due_jobs(self, now):
+ timestamp = datetime_to_utc_timestamp(now)
+ jobs = [job_def['job'] for job_def in self._get_jobs()
+ if job_def['next_run_time'] is not None and job_def['next_run_time'] <= timestamp]
+ return jobs
+
+ def get_next_run_time(self):
+ next_runs = [job_def['next_run_time'] for job_def in self._get_jobs()
+ if job_def['next_run_time'] is not None]
+ return utc_timestamp_to_datetime(min(next_runs)) if len(next_runs) > 0 else None
+
+ def get_all_jobs(self):
+ jobs = [job_def['job'] for job_def in self._get_jobs()]
+ self._fix_paused_jobs_sorting(jobs)
+ return jobs
+
+ def add_job(self, job):
+ self._ensure_paths()
+ node_path = self.path + "/" + str(job.id)
+ value = {
+ 'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
+ 'job_state': job.__getstate__()
+ }
+ data = pickle.dumps(value, self.pickle_protocol)
+ try:
+ self.client.create(node_path, value=data)
+ except NodeExistsError:
+ raise ConflictingIdError(job.id)
+
+ def update_job(self, job):
+ self._ensure_paths()
+ node_path = self.path + "/" + str(job.id)
+ changes = {
+ 'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
+ 'job_state': job.__getstate__()
+ }
+ data = pickle.dumps(changes, self.pickle_protocol)
+ try:
+ self.client.set(node_path, value=data)
+ except NoNodeError:
+ raise JobLookupError(job.id)
+
+ def remove_job(self, job_id):
+ self._ensure_paths()
+ node_path = self.path + "/" + str(job_id)
+ try:
+ self.client.delete(node_path)
+ except NoNodeError:
+ raise JobLookupError(job_id)
+
+ def remove_all_jobs(self):
+ try:
+ self.client.delete(self.path, recursive=True)
+ except NoNodeError:
+ pass
+ self._ensured_path = False
+
+ def shutdown(self):
+ if self.close_connection_on_exit:
+ self.client.stop()
+ self.client.close()
+
+ def _reconstitute_job(self, job_state):
+ job_state = job_state
+ job = Job.__new__(Job)
+ job.__setstate__(job_state)
+ job._scheduler = self._scheduler
+ job._jobstore_alias = self._alias
+ return job
+
+ def _get_jobs(self):
+ self._ensure_paths()
+ jobs = []
+ failed_job_ids = []
+ all_ids = self.client.get_children(self.path)
+ for node_name in all_ids:
+ try:
+ node_path = self.path + "/" + node_name
+ content, _ = self.client.get(node_path)
+ doc = pickle.loads(content)
+ job_def = {
+ 'job_id': node_name,
+ 'next_run_time': doc['next_run_time'] if doc['next_run_time'] else None,
+ 'job_state': doc['job_state'],
+ 'job': self._reconstitute_job(doc['job_state']),
+ 'creation_time': _.ctime
+ }
+ jobs.append(job_def)
+ except BaseException:
+ self._logger.exception('Unable to restore job "%s" -- removing it' % node_name)
+ failed_job_ids.append(node_name)
+
+ # Remove all the jobs we failed to restore
+ if failed_job_ids:
+ for failed_id in failed_job_ids:
+ self.remove_job(failed_id)
+ paused_sort_key = datetime(9999, 12, 31, tzinfo=utc)
+ return sorted(jobs, key=lambda job_def: (job_def['job'].next_run_time or paused_sort_key,
+ job_def['creation_time']))
+
+ def __repr__(self):
+ self._logger.exception('<%s (client=%s)>' % (self.__class__.__name__, self.client))
+ return '<%s (client=%s)>' % (self.__class__.__name__, self.client)
diff --git a/telegramer/include/apscheduler/schedulers/__init__.py b/telegramer/include/apscheduler/schedulers/__init__.py
new file mode 100644
index 0000000..bd8a790
--- /dev/null
+++ b/telegramer/include/apscheduler/schedulers/__init__.py
@@ -0,0 +1,12 @@
+class SchedulerAlreadyRunningError(Exception):
+ """Raised when attempting to start or configure the scheduler when it's already running."""
+
+ def __str__(self):
+ return 'Scheduler is already running'
+
+
+class SchedulerNotRunningError(Exception):
+ """Raised when attempting to shutdown the scheduler when it's not running."""
+
+ def __str__(self):
+ return 'Scheduler is not running'
diff --git a/telegramer/include/apscheduler/schedulers/asyncio.py b/telegramer/include/apscheduler/schedulers/asyncio.py
new file mode 100644
index 0000000..70ebede
--- /dev/null
+++ b/telegramer/include/apscheduler/schedulers/asyncio.py
@@ -0,0 +1,74 @@
+from __future__ import absolute_import
+from functools import wraps, partial
+
+from apscheduler.schedulers.base import BaseScheduler
+from apscheduler.util import maybe_ref
+
+try:
+ import asyncio
+except ImportError: # pragma: nocover
+ try:
+ import trollius as asyncio
+ except ImportError:
+ raise ImportError(
+ 'AsyncIOScheduler requires either Python 3.4 or the asyncio package installed')
+
+
+def run_in_event_loop(func):
+ @wraps(func)
+ def wrapper(self, *args, **kwargs):
+ wrapped = partial(func, self, *args, **kwargs)
+ self._eventloop.call_soon_threadsafe(wrapped)
+ return wrapper
+
+
+class AsyncIOScheduler(BaseScheduler):
+ """
+ A scheduler that runs on an asyncio (:pep:`3156`) event loop.
+
+ The default executor can run jobs based on native coroutines (``async def``).
+
+ Extra options:
+
+ ============== =============================================================
+ ``event_loop`` AsyncIO event loop to use (defaults to the global event loop)
+ ============== =============================================================
+ """
+
+ _eventloop = None
+ _timeout = None
+
+ def start(self, paused=False):
+ if not self._eventloop:
+ self._eventloop = asyncio.get_event_loop()
+
+ super(AsyncIOScheduler, self).start(paused)
+
+ @run_in_event_loop
+ def shutdown(self, wait=True):
+ super(AsyncIOScheduler, self).shutdown(wait)
+ self._stop_timer()
+
+ def _configure(self, config):
+ self._eventloop = maybe_ref(config.pop('event_loop', None))
+ super(AsyncIOScheduler, self)._configure(config)
+
+ def _start_timer(self, wait_seconds):
+ self._stop_timer()
+ if wait_seconds is not None:
+ self._timeout = self._eventloop.call_later(wait_seconds, self.wakeup)
+
+ def _stop_timer(self):
+ if self._timeout:
+ self._timeout.cancel()
+ del self._timeout
+
+ @run_in_event_loop
+ def wakeup(self):
+ self._stop_timer()
+ wait_seconds = self._process_jobs()
+ self._start_timer(wait_seconds)
+
+ def _create_default_executor(self):
+ from apscheduler.executors.asyncio import AsyncIOExecutor
+ return AsyncIOExecutor()
diff --git a/telegramer/include/apscheduler/schedulers/background.py b/telegramer/include/apscheduler/schedulers/background.py
new file mode 100644
index 0000000..bb8f77d
--- /dev/null
+++ b/telegramer/include/apscheduler/schedulers/background.py
@@ -0,0 +1,43 @@
+from __future__ import absolute_import
+
+from threading import Thread, Event
+
+from apscheduler.schedulers.base import BaseScheduler
+from apscheduler.schedulers.blocking import BlockingScheduler
+from apscheduler.util import asbool
+
+
+class BackgroundScheduler(BlockingScheduler):
+ """
+ A scheduler that runs in the background using a separate thread
+ (:meth:`~apscheduler.schedulers.base.BaseScheduler.start` will return immediately).
+
+ Extra options:
+
+ ========== =============================================================================
+ ``daemon`` Set the ``daemon`` option in the background thread (defaults to ``True``, see
+ `the documentation
+ `_
+ for further details)
+ ========== =============================================================================
+ """
+
+ _thread = None
+
+ def _configure(self, config):
+ self._daemon = asbool(config.pop('daemon', True))
+ super(BackgroundScheduler, self)._configure(config)
+
+ def start(self, *args, **kwargs):
+ if self._event is None or self._event.is_set():
+ self._event = Event()
+
+ BaseScheduler.start(self, *args, **kwargs)
+ self._thread = Thread(target=self._main_loop, name='APScheduler')
+ self._thread.daemon = self._daemon
+ self._thread.start()
+
+ def shutdown(self, *args, **kwargs):
+ super(BackgroundScheduler, self).shutdown(*args, **kwargs)
+ self._thread.join()
+ del self._thread
diff --git a/telegramer/include/apscheduler/schedulers/base.py b/telegramer/include/apscheduler/schedulers/base.py
new file mode 100644
index 0000000..444de8e
--- /dev/null
+++ b/telegramer/include/apscheduler/schedulers/base.py
@@ -0,0 +1,1026 @@
+from __future__ import print_function
+
+from abc import ABCMeta, abstractmethod
+from threading import RLock
+from datetime import datetime, timedelta
+from logging import getLogger
+import warnings
+import sys
+
+from pkg_resources import iter_entry_points
+from tzlocal import get_localzone
+import six
+
+from apscheduler.schedulers import SchedulerAlreadyRunningError, SchedulerNotRunningError
+from apscheduler.executors.base import MaxInstancesReachedError, BaseExecutor
+from apscheduler.executors.pool import ThreadPoolExecutor
+from apscheduler.jobstores.base import ConflictingIdError, JobLookupError, BaseJobStore
+from apscheduler.jobstores.memory import MemoryJobStore
+from apscheduler.job import Job
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.util import (
+ asbool, asint, astimezone, maybe_ref, timedelta_seconds, undefined, TIMEOUT_MAX)
+from apscheduler.events import (
+ SchedulerEvent, JobEvent, JobSubmissionEvent, EVENT_SCHEDULER_START, EVENT_SCHEDULER_SHUTDOWN,
+ EVENT_JOBSTORE_ADDED, EVENT_JOBSTORE_REMOVED, EVENT_ALL, EVENT_JOB_MODIFIED, EVENT_JOB_REMOVED,
+ EVENT_JOB_ADDED, EVENT_EXECUTOR_ADDED, EVENT_EXECUTOR_REMOVED, EVENT_ALL_JOBS_REMOVED,
+ EVENT_JOB_SUBMITTED, EVENT_JOB_MAX_INSTANCES, EVENT_SCHEDULER_RESUMED, EVENT_SCHEDULER_PAUSED)
+
+try:
+ from collections.abc import MutableMapping
+except ImportError:
+ from collections import MutableMapping
+
+#: constant indicating a scheduler's stopped state
+STATE_STOPPED = 0
+#: constant indicating a scheduler's running state (started and processing jobs)
+STATE_RUNNING = 1
+#: constant indicating a scheduler's paused state (started but not processing jobs)
+STATE_PAUSED = 2
+
+
+class BaseScheduler(six.with_metaclass(ABCMeta)):
+ """
+ Abstract base class for all schedulers.
+
+ Takes the following keyword arguments:
+
+ :param str|logging.Logger logger: logger to use for the scheduler's logging (defaults to
+ apscheduler.scheduler)
+ :param str|datetime.tzinfo timezone: the default time zone (defaults to the local timezone)
+ :param int|float jobstore_retry_interval: the minimum number of seconds to wait between
+ retries in the scheduler's main loop if the job store raises an exception when getting
+ the list of due jobs
+ :param dict job_defaults: default values for newly added jobs
+ :param dict jobstores: a dictionary of job store alias -> job store instance or configuration
+ dict
+ :param dict executors: a dictionary of executor alias -> executor instance or configuration
+ dict
+
+ :ivar int state: current running state of the scheduler (one of the following constants from
+ ``apscheduler.schedulers.base``: ``STATE_STOPPED``, ``STATE_RUNNING``, ``STATE_PAUSED``)
+
+ .. seealso:: :ref:`scheduler-config`
+ """
+
+ _trigger_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.triggers'))
+ _trigger_classes = {}
+ _executor_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.executors'))
+ _executor_classes = {}
+ _jobstore_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.jobstores'))
+ _jobstore_classes = {}
+
+ #
+ # Public API
+ #
+
+ def __init__(self, gconfig={}, **options):
+ super(BaseScheduler, self).__init__()
+ self._executors = {}
+ self._executors_lock = self._create_lock()
+ self._jobstores = {}
+ self._jobstores_lock = self._create_lock()
+ self._listeners = []
+ self._listeners_lock = self._create_lock()
+ self._pending_jobs = []
+ self.state = STATE_STOPPED
+ self.configure(gconfig, **options)
+
+ def __getstate__(self):
+ raise TypeError("Schedulers cannot be serialized. Ensure that you are not passing a "
+ "scheduler instance as an argument to a job, or scheduling an instance "
+ "method where the instance contains a scheduler as an attribute.")
+
+ def configure(self, gconfig={}, prefix='apscheduler.', **options):
+ """
+ Reconfigures the scheduler with the given options.
+
+ Can only be done when the scheduler isn't running.
+
+ :param dict gconfig: a "global" configuration dictionary whose values can be overridden by
+ keyword arguments to this method
+ :param str|unicode prefix: pick only those keys from ``gconfig`` that are prefixed with
+ this string (pass an empty string or ``None`` to use all keys)
+ :raises SchedulerAlreadyRunningError: if the scheduler is already running
+
+ """
+ if self.state != STATE_STOPPED:
+ raise SchedulerAlreadyRunningError
+
+ # If a non-empty prefix was given, strip it from the keys in the
+ # global configuration dict
+ if prefix:
+ prefixlen = len(prefix)
+ gconfig = dict((key[prefixlen:], value) for key, value in six.iteritems(gconfig)
+ if key.startswith(prefix))
+
+ # Create a structure from the dotted options
+ # (e.g. "a.b.c = d" -> {'a': {'b': {'c': 'd'}}})
+ config = {}
+ for key, value in six.iteritems(gconfig):
+ parts = key.split('.')
+ parent = config
+ key = parts.pop(0)
+ while parts:
+ parent = parent.setdefault(key, {})
+ key = parts.pop(0)
+ parent[key] = value
+
+ # Override any options with explicit keyword arguments
+ config.update(options)
+ self._configure(config)
+
+ def start(self, paused=False):
+ """
+ Start the configured executors and job stores and begin processing scheduled jobs.
+
+ :param bool paused: if ``True``, don't start job processing until :meth:`resume` is called
+ :raises SchedulerAlreadyRunningError: if the scheduler is already running
+ :raises RuntimeError: if running under uWSGI with threads disabled
+
+ """
+ if self.state != STATE_STOPPED:
+ raise SchedulerAlreadyRunningError
+
+ self._check_uwsgi()
+
+ with self._executors_lock:
+ # Create a default executor if nothing else is configured
+ if 'default' not in self._executors:
+ self.add_executor(self._create_default_executor(), 'default')
+
+ # Start all the executors
+ for alias, executor in six.iteritems(self._executors):
+ executor.start(self, alias)
+
+ with self._jobstores_lock:
+ # Create a default job store if nothing else is configured
+ if 'default' not in self._jobstores:
+ self.add_jobstore(self._create_default_jobstore(), 'default')
+
+ # Start all the job stores
+ for alias, store in six.iteritems(self._jobstores):
+ store.start(self, alias)
+
+ # Schedule all pending jobs
+ for job, jobstore_alias, replace_existing in self._pending_jobs:
+ self._real_add_job(job, jobstore_alias, replace_existing)
+ del self._pending_jobs[:]
+
+ self.state = STATE_PAUSED if paused else STATE_RUNNING
+ self._logger.info('Scheduler started')
+ self._dispatch_event(SchedulerEvent(EVENT_SCHEDULER_START))
+
+ if not paused:
+ self.wakeup()
+
+ @abstractmethod
+ def shutdown(self, wait=True):
+ """
+ Shuts down the scheduler, along with its executors and job stores.
+
+ Does not interrupt any currently running jobs.
+
+ :param bool wait: ``True`` to wait until all currently executing jobs have finished
+ :raises SchedulerNotRunningError: if the scheduler has not been started yet
+
+ """
+ if self.state == STATE_STOPPED:
+ raise SchedulerNotRunningError
+
+ self.state = STATE_STOPPED
+
+ # Shut down all executors
+ with self._executors_lock, self._jobstores_lock:
+ for executor in six.itervalues(self._executors):
+ executor.shutdown(wait)
+
+ # Shut down all job stores
+ for jobstore in six.itervalues(self._jobstores):
+ jobstore.shutdown()
+
+ self._logger.info('Scheduler has been shut down')
+ self._dispatch_event(SchedulerEvent(EVENT_SCHEDULER_SHUTDOWN))
+
+ def pause(self):
+ """
+ Pause job processing in the scheduler.
+
+ This will prevent the scheduler from waking up to do job processing until :meth:`resume`
+ is called. It will not however stop any already running job processing.
+
+ """
+ if self.state == STATE_STOPPED:
+ raise SchedulerNotRunningError
+ elif self.state == STATE_RUNNING:
+ self.state = STATE_PAUSED
+ self._logger.info('Paused scheduler job processing')
+ self._dispatch_event(SchedulerEvent(EVENT_SCHEDULER_PAUSED))
+
+ def resume(self):
+ """Resume job processing in the scheduler."""
+ if self.state == STATE_STOPPED:
+ raise SchedulerNotRunningError
+ elif self.state == STATE_PAUSED:
+ self.state = STATE_RUNNING
+ self._logger.info('Resumed scheduler job processing')
+ self._dispatch_event(SchedulerEvent(EVENT_SCHEDULER_RESUMED))
+ self.wakeup()
+
+ @property
+ def running(self):
+ """
+ Return ``True`` if the scheduler has been started.
+
+ This is a shortcut for ``scheduler.state != STATE_STOPPED``.
+
+ """
+ return self.state != STATE_STOPPED
+
+ def add_executor(self, executor, alias='default', **executor_opts):
+ """
+ Adds an executor to this scheduler.
+
+ Any extra keyword arguments will be passed to the executor plugin's constructor, assuming
+ that the first argument is the name of an executor plugin.
+
+ :param str|unicode|apscheduler.executors.base.BaseExecutor executor: either an executor
+ instance or the name of an executor plugin
+ :param str|unicode alias: alias for the scheduler
+ :raises ValueError: if there is already an executor by the given alias
+
+ """
+ with self._executors_lock:
+ if alias in self._executors:
+ raise ValueError('This scheduler already has an executor by the alias of "%s"' %
+ alias)
+
+ if isinstance(executor, BaseExecutor):
+ self._executors[alias] = executor
+ elif isinstance(executor, six.string_types):
+ self._executors[alias] = executor = self._create_plugin_instance(
+ 'executor', executor, executor_opts)
+ else:
+ raise TypeError('Expected an executor instance or a string, got %s instead' %
+ executor.__class__.__name__)
+
+ # Start the executor right away if the scheduler is running
+ if self.state != STATE_STOPPED:
+ executor.start(self, alias)
+
+ self._dispatch_event(SchedulerEvent(EVENT_EXECUTOR_ADDED, alias))
+
+ def remove_executor(self, alias, shutdown=True):
+ """
+ Removes the executor by the given alias from this scheduler.
+
+ :param str|unicode alias: alias of the executor
+ :param bool shutdown: ``True`` to shut down the executor after
+ removing it
+
+ """
+ with self._executors_lock:
+ executor = self._lookup_executor(alias)
+ del self._executors[alias]
+
+ if shutdown:
+ executor.shutdown()
+
+ self._dispatch_event(SchedulerEvent(EVENT_EXECUTOR_REMOVED, alias))
+
+ def add_jobstore(self, jobstore, alias='default', **jobstore_opts):
+ """
+ Adds a job store to this scheduler.
+
+ Any extra keyword arguments will be passed to the job store plugin's constructor, assuming
+ that the first argument is the name of a job store plugin.
+
+ :param str|unicode|apscheduler.jobstores.base.BaseJobStore jobstore: job store to be added
+ :param str|unicode alias: alias for the job store
+ :raises ValueError: if there is already a job store by the given alias
+
+ """
+ with self._jobstores_lock:
+ if alias in self._jobstores:
+ raise ValueError('This scheduler already has a job store by the alias of "%s"' %
+ alias)
+
+ if isinstance(jobstore, BaseJobStore):
+ self._jobstores[alias] = jobstore
+ elif isinstance(jobstore, six.string_types):
+ self._jobstores[alias] = jobstore = self._create_plugin_instance(
+ 'jobstore', jobstore, jobstore_opts)
+ else:
+ raise TypeError('Expected a job store instance or a string, got %s instead' %
+ jobstore.__class__.__name__)
+
+ # Start the job store right away if the scheduler isn't stopped
+ if self.state != STATE_STOPPED:
+ jobstore.start(self, alias)
+
+ # Notify listeners that a new job store has been added
+ self._dispatch_event(SchedulerEvent(EVENT_JOBSTORE_ADDED, alias))
+
+ # Notify the scheduler so it can scan the new job store for jobs
+ if self.state != STATE_STOPPED:
+ self.wakeup()
+
+ def remove_jobstore(self, alias, shutdown=True):
+ """
+ Removes the job store by the given alias from this scheduler.
+
+ :param str|unicode alias: alias of the job store
+ :param bool shutdown: ``True`` to shut down the job store after removing it
+
+ """
+ with self._jobstores_lock:
+ jobstore = self._lookup_jobstore(alias)
+ del self._jobstores[alias]
+
+ if shutdown:
+ jobstore.shutdown()
+
+ self._dispatch_event(SchedulerEvent(EVENT_JOBSTORE_REMOVED, alias))
+
+ def add_listener(self, callback, mask=EVENT_ALL):
+ """
+ add_listener(callback, mask=EVENT_ALL)
+
+ Adds a listener for scheduler events.
+
+ When a matching event occurs, ``callback`` is executed with the event object as its
+ sole argument. If the ``mask`` parameter is not provided, the callback will receive events
+ of all types.
+
+ :param callback: any callable that takes one argument
+ :param int mask: bitmask that indicates which events should be
+ listened to
+
+ .. seealso:: :mod:`apscheduler.events`
+ .. seealso:: :ref:`scheduler-events`
+
+ """
+ with self._listeners_lock:
+ self._listeners.append((callback, mask))
+
+ def remove_listener(self, callback):
+ """Removes a previously added event listener."""
+
+ with self._listeners_lock:
+ for i, (cb, _) in enumerate(self._listeners):
+ if callback == cb:
+ del self._listeners[i]
+
+ def add_job(self, func, trigger=None, args=None, kwargs=None, id=None, name=None,
+ misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined,
+ next_run_time=undefined, jobstore='default', executor='default',
+ replace_existing=False, **trigger_args):
+ """
+ add_job(func, trigger=None, args=None, kwargs=None, id=None, \
+ name=None, misfire_grace_time=undefined, coalesce=undefined, \
+ max_instances=undefined, next_run_time=undefined, \
+ jobstore='default', executor='default', \
+ replace_existing=False, **trigger_args)
+
+ Adds the given job to the job list and wakes up the scheduler if it's already running.
+
+ Any option that defaults to ``undefined`` will be replaced with the corresponding default
+ value when the job is scheduled (which happens when the scheduler is started, or
+ immediately if the scheduler is already running).
+
+ The ``func`` argument can be given either as a callable object or a textual reference in
+ the ``package.module:some.object`` format, where the first half (separated by ``:``) is an
+ importable module and the second half is a reference to the callable object, relative to
+ the module.
+
+ The ``trigger`` argument can either be:
+ #. the alias name of the trigger (e.g. ``date``, ``interval`` or ``cron``), in which case
+ any extra keyword arguments to this method are passed on to the trigger's constructor
+ #. an instance of a trigger class
+
+ :param func: callable (or a textual reference to one) to run at the given time
+ :param str|apscheduler.triggers.base.BaseTrigger trigger: trigger that determines when
+ ``func`` is called
+ :param list|tuple args: list of positional arguments to call func with
+ :param dict kwargs: dict of keyword arguments to call func with
+ :param str|unicode id: explicit identifier for the job (for modifying it later)
+ :param str|unicode name: textual description of the job
+ :param int misfire_grace_time: seconds after the designated runtime that the job is still
+ allowed to be run (or ``None`` to allow the job to run no matter how late it is)
+ :param bool coalesce: run once instead of many times if the scheduler determines that the
+ job should be run more than once in succession
+ :param int max_instances: maximum number of concurrently running instances allowed for this
+ job
+ :param datetime next_run_time: when to first run the job, regardless of the trigger (pass
+ ``None`` to add the job as paused)
+ :param str|unicode jobstore: alias of the job store to store the job in
+ :param str|unicode executor: alias of the executor to run the job with
+ :param bool replace_existing: ``True`` to replace an existing job with the same ``id``
+ (but retain the number of runs from the existing one)
+ :rtype: Job
+
+ """
+ job_kwargs = {
+ 'trigger': self._create_trigger(trigger, trigger_args),
+ 'executor': executor,
+ 'func': func,
+ 'args': tuple(args) if args is not None else (),
+ 'kwargs': dict(kwargs) if kwargs is not None else {},
+ 'id': id,
+ 'name': name,
+ 'misfire_grace_time': misfire_grace_time,
+ 'coalesce': coalesce,
+ 'max_instances': max_instances,
+ 'next_run_time': next_run_time
+ }
+ job_kwargs = dict((key, value) for key, value in six.iteritems(job_kwargs) if
+ value is not undefined)
+ job = Job(self, **job_kwargs)
+
+ # Don't really add jobs to job stores before the scheduler is up and running
+ with self._jobstores_lock:
+ if self.state == STATE_STOPPED:
+ self._pending_jobs.append((job, jobstore, replace_existing))
+ self._logger.info('Adding job tentatively -- it will be properly scheduled when '
+ 'the scheduler starts')
+ else:
+ self._real_add_job(job, jobstore, replace_existing)
+
+ return job
+
+ def scheduled_job(self, trigger, args=None, kwargs=None, id=None, name=None,
+ misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined,
+ next_run_time=undefined, jobstore='default', executor='default',
+ **trigger_args):
+ """
+ scheduled_job(trigger, args=None, kwargs=None, id=None, \
+ name=None, misfire_grace_time=undefined, \
+ coalesce=undefined, max_instances=undefined, \
+ next_run_time=undefined, jobstore='default', \
+ executor='default',**trigger_args)
+
+ A decorator version of :meth:`add_job`, except that ``replace_existing`` is always
+ ``True``.
+
+ .. important:: The ``id`` argument must be given if scheduling a job in a persistent job
+ store. The scheduler cannot, however, enforce this requirement.
+
+ """
+ def inner(func):
+ self.add_job(func, trigger, args, kwargs, id, name, misfire_grace_time, coalesce,
+ max_instances, next_run_time, jobstore, executor, True, **trigger_args)
+ return func
+ return inner
+
+ def modify_job(self, job_id, jobstore=None, **changes):
+ """
+ Modifies the properties of a single job.
+
+ Modifications are passed to this method as extra keyword arguments.
+
+ :param str|unicode job_id: the identifier of the job
+ :param str|unicode jobstore: alias of the job store that contains the job
+ :return Job: the relevant job instance
+
+ """
+ with self._jobstores_lock:
+ job, jobstore = self._lookup_job(job_id, jobstore)
+ job._modify(**changes)
+ if jobstore:
+ self._lookup_jobstore(jobstore).update_job(job)
+
+ self._dispatch_event(JobEvent(EVENT_JOB_MODIFIED, job_id, jobstore))
+
+ # Wake up the scheduler since the job's next run time may have been changed
+ if self.state == STATE_RUNNING:
+ self.wakeup()
+
+ return job
+
+ def reschedule_job(self, job_id, jobstore=None, trigger=None, **trigger_args):
+ """
+ Constructs a new trigger for a job and updates its next run time.
+
+ Extra keyword arguments are passed directly to the trigger's constructor.
+
+ :param str|unicode job_id: the identifier of the job
+ :param str|unicode jobstore: alias of the job store that contains the job
+ :param trigger: alias of the trigger type or a trigger instance
+ :return Job: the relevant job instance
+
+ """
+ trigger = self._create_trigger(trigger, trigger_args)
+ now = datetime.now(self.timezone)
+ next_run_time = trigger.get_next_fire_time(None, now)
+ return self.modify_job(job_id, jobstore, trigger=trigger, next_run_time=next_run_time)
+
+ def pause_job(self, job_id, jobstore=None):
+ """
+ Causes the given job not to be executed until it is explicitly resumed.
+
+ :param str|unicode job_id: the identifier of the job
+ :param str|unicode jobstore: alias of the job store that contains the job
+ :return Job: the relevant job instance
+
+ """
+ return self.modify_job(job_id, jobstore, next_run_time=None)
+
+ def resume_job(self, job_id, jobstore=None):
+ """
+ Resumes the schedule of the given job, or removes the job if its schedule is finished.
+
+ :param str|unicode job_id: the identifier of the job
+ :param str|unicode jobstore: alias of the job store that contains the job
+ :return Job|None: the relevant job instance if the job was rescheduled, or ``None`` if no
+ next run time could be calculated and the job was removed
+
+ """
+ with self._jobstores_lock:
+ job, jobstore = self._lookup_job(job_id, jobstore)
+ now = datetime.now(self.timezone)
+ next_run_time = job.trigger.get_next_fire_time(None, now)
+ if next_run_time:
+ return self.modify_job(job_id, jobstore, next_run_time=next_run_time)
+ else:
+ self.remove_job(job.id, jobstore)
+
+ def get_jobs(self, jobstore=None, pending=None):
+ """
+ Returns a list of pending jobs (if the scheduler hasn't been started yet) and scheduled
+ jobs, either from a specific job store or from all of them.
+
+ If the scheduler has not been started yet, only pending jobs can be returned because the
+ job stores haven't been started yet either.
+
+ :param str|unicode jobstore: alias of the job store
+ :param bool pending: **DEPRECATED**
+ :rtype: list[Job]
+
+ """
+ if pending is not None:
+ warnings.warn('The "pending" option is deprecated -- get_jobs() always returns '
+ 'scheduled jobs if the scheduler has been started and pending jobs '
+ 'otherwise', DeprecationWarning)
+
+ with self._jobstores_lock:
+ jobs = []
+ if self.state == STATE_STOPPED:
+ for job, alias, replace_existing in self._pending_jobs:
+ if jobstore is None or alias == jobstore:
+ jobs.append(job)
+ else:
+ for alias, store in six.iteritems(self._jobstores):
+ if jobstore is None or alias == jobstore:
+ jobs.extend(store.get_all_jobs())
+
+ return jobs
+
+ def get_job(self, job_id, jobstore=None):
+ """
+ Returns the Job that matches the given ``job_id``.
+
+ :param str|unicode job_id: the identifier of the job
+ :param str|unicode jobstore: alias of the job store that most likely contains the job
+ :return: the Job by the given ID, or ``None`` if it wasn't found
+ :rtype: Job
+
+ """
+ with self._jobstores_lock:
+ try:
+ return self._lookup_job(job_id, jobstore)[0]
+ except JobLookupError:
+ return
+
+ def remove_job(self, job_id, jobstore=None):
+ """
+ Removes a job, preventing it from being run any more.
+
+ :param str|unicode job_id: the identifier of the job
+ :param str|unicode jobstore: alias of the job store that contains the job
+ :raises JobLookupError: if the job was not found
+
+ """
+ jobstore_alias = None
+ with self._jobstores_lock:
+ # Check if the job is among the pending jobs
+ if self.state == STATE_STOPPED:
+ for i, (job, alias, replace_existing) in enumerate(self._pending_jobs):
+ if job.id == job_id and jobstore in (None, alias):
+ del self._pending_jobs[i]
+ jobstore_alias = alias
+ break
+ else:
+ # Otherwise, try to remove it from each store until it succeeds or we run out of
+ # stores to check
+ for alias, store in six.iteritems(self._jobstores):
+ if jobstore in (None, alias):
+ try:
+ store.remove_job(job_id)
+ jobstore_alias = alias
+ break
+ except JobLookupError:
+ continue
+
+ if jobstore_alias is None:
+ raise JobLookupError(job_id)
+
+ # Notify listeners that a job has been removed
+ event = JobEvent(EVENT_JOB_REMOVED, job_id, jobstore_alias)
+ self._dispatch_event(event)
+
+ self._logger.info('Removed job %s', job_id)
+
+ def remove_all_jobs(self, jobstore=None):
+ """
+ Removes all jobs from the specified job store, or all job stores if none is given.
+
+ :param str|unicode jobstore: alias of the job store
+
+ """
+ with self._jobstores_lock:
+ if self.state == STATE_STOPPED:
+ if jobstore:
+ self._pending_jobs = [pending for pending in self._pending_jobs if
+ pending[1] != jobstore]
+ else:
+ self._pending_jobs = []
+ else:
+ for alias, store in six.iteritems(self._jobstores):
+ if jobstore in (None, alias):
+ store.remove_all_jobs()
+
+ self._dispatch_event(SchedulerEvent(EVENT_ALL_JOBS_REMOVED, jobstore))
+
+ def print_jobs(self, jobstore=None, out=None):
+ """
+ print_jobs(jobstore=None, out=sys.stdout)
+
+ Prints out a textual listing of all jobs currently scheduled on either all job stores or
+ just a specific one.
+
+ :param str|unicode jobstore: alias of the job store, ``None`` to list jobs from all stores
+ :param file out: a file-like object to print to (defaults to **sys.stdout** if nothing is
+ given)
+
+ """
+ out = out or sys.stdout
+ with self._jobstores_lock:
+ if self.state == STATE_STOPPED:
+ print(u'Pending jobs:', file=out)
+ if self._pending_jobs:
+ for job, jobstore_alias, replace_existing in self._pending_jobs:
+ if jobstore in (None, jobstore_alias):
+ print(u' %s' % job, file=out)
+ else:
+ print(u' No pending jobs', file=out)
+ else:
+ for alias, store in sorted(six.iteritems(self._jobstores)):
+ if jobstore in (None, alias):
+ print(u'Jobstore %s:' % alias, file=out)
+ jobs = store.get_all_jobs()
+ if jobs:
+ for job in jobs:
+ print(u' %s' % job, file=out)
+ else:
+ print(u' No scheduled jobs', file=out)
+
+ @abstractmethod
+ def wakeup(self):
+ """
+ Notifies the scheduler that there may be jobs due for execution.
+ Triggers :meth:`_process_jobs` to be run in an implementation specific manner.
+ """
+
+ #
+ # Private API
+ #
+
+ def _configure(self, config):
+ # Set general options
+ self._logger = maybe_ref(config.pop('logger', None)) or getLogger('apscheduler.scheduler')
+ self.timezone = astimezone(config.pop('timezone', None)) or get_localzone()
+ self.jobstore_retry_interval = float(config.pop('jobstore_retry_interval', 10))
+
+ # Set the job defaults
+ job_defaults = config.get('job_defaults', {})
+ self._job_defaults = {
+ 'misfire_grace_time': asint(job_defaults.get('misfire_grace_time', 1)),
+ 'coalesce': asbool(job_defaults.get('coalesce', True)),
+ 'max_instances': asint(job_defaults.get('max_instances', 1))
+ }
+
+ # Configure executors
+ self._executors.clear()
+ for alias, value in six.iteritems(config.get('executors', {})):
+ if isinstance(value, BaseExecutor):
+ self.add_executor(value, alias)
+ elif isinstance(value, MutableMapping):
+ executor_class = value.pop('class', None)
+ plugin = value.pop('type', None)
+ if plugin:
+ executor = self._create_plugin_instance('executor', plugin, value)
+ elif executor_class:
+ cls = maybe_ref(executor_class)
+ executor = cls(**value)
+ else:
+ raise ValueError(
+ 'Cannot create executor "%s" -- either "type" or "class" must be defined' %
+ alias)
+
+ self.add_executor(executor, alias)
+ else:
+ raise TypeError(
+ "Expected executor instance or dict for executors['%s'], got %s instead" %
+ (alias, value.__class__.__name__))
+
+ # Configure job stores
+ self._jobstores.clear()
+ for alias, value in six.iteritems(config.get('jobstores', {})):
+ if isinstance(value, BaseJobStore):
+ self.add_jobstore(value, alias)
+ elif isinstance(value, MutableMapping):
+ jobstore_class = value.pop('class', None)
+ plugin = value.pop('type', None)
+ if plugin:
+ jobstore = self._create_plugin_instance('jobstore', plugin, value)
+ elif jobstore_class:
+ cls = maybe_ref(jobstore_class)
+ jobstore = cls(**value)
+ else:
+ raise ValueError(
+ 'Cannot create job store "%s" -- either "type" or "class" must be '
+ 'defined' % alias)
+
+ self.add_jobstore(jobstore, alias)
+ else:
+ raise TypeError(
+ "Expected job store instance or dict for jobstores['%s'], got %s instead" %
+ (alias, value.__class__.__name__))
+
+ def _create_default_executor(self):
+ """Creates a default executor store, specific to the particular scheduler type."""
+ return ThreadPoolExecutor()
+
+ def _create_default_jobstore(self):
+ """Creates a default job store, specific to the particular scheduler type."""
+ return MemoryJobStore()
+
+ def _lookup_executor(self, alias):
+ """
+ Returns the executor instance by the given name from the list of executors that were added
+ to this scheduler.
+
+ :type alias: str
+ :raises KeyError: if no executor by the given alias is not found
+
+ """
+ try:
+ return self._executors[alias]
+ except KeyError:
+ raise KeyError('No such executor: %s' % alias)
+
+ def _lookup_jobstore(self, alias):
+ """
+ Returns the job store instance by the given name from the list of job stores that were
+ added to this scheduler.
+
+ :type alias: str
+ :raises KeyError: if no job store by the given alias is not found
+
+ """
+ try:
+ return self._jobstores[alias]
+ except KeyError:
+ raise KeyError('No such job store: %s' % alias)
+
+ def _lookup_job(self, job_id, jobstore_alias):
+ """
+ Finds a job by its ID.
+
+ :type job_id: str
+ :param str jobstore_alias: alias of a job store to look in
+ :return tuple[Job, str]: a tuple of job, jobstore alias (jobstore alias is None in case of
+ a pending job)
+ :raises JobLookupError: if no job by the given ID is found.
+
+ """
+ if self.state == STATE_STOPPED:
+ # Check if the job is among the pending jobs
+ for job, alias, replace_existing in self._pending_jobs:
+ if job.id == job_id:
+ return job, None
+ else:
+ # Look in all job stores
+ for alias, store in six.iteritems(self._jobstores):
+ if jobstore_alias in (None, alias):
+ job = store.lookup_job(job_id)
+ if job is not None:
+ return job, alias
+
+ raise JobLookupError(job_id)
+
+ def _dispatch_event(self, event):
+ """
+ Dispatches the given event to interested listeners.
+
+ :param SchedulerEvent event: the event to send
+
+ """
+ with self._listeners_lock:
+ listeners = tuple(self._listeners)
+
+ for cb, mask in listeners:
+ if event.code & mask:
+ try:
+ cb(event)
+ except BaseException:
+ self._logger.exception('Error notifying listener')
+
+ def _check_uwsgi(self):
+ """Check if we're running under uWSGI with threads disabled."""
+ uwsgi_module = sys.modules.get('uwsgi')
+ if not getattr(uwsgi_module, 'has_threads', True):
+ raise RuntimeError('The scheduler seems to be running under uWSGI, but threads have '
+ 'been disabled. You must run uWSGI with the --enable-threads '
+ 'option for the scheduler to work.')
+
+ def _real_add_job(self, job, jobstore_alias, replace_existing):
+ """
+ :param Job job: the job to add
+ :param bool replace_existing: ``True`` to use update_job() in case the job already exists
+ in the store
+
+ """
+ # Fill in undefined values with defaults
+ replacements = {}
+ for key, value in six.iteritems(self._job_defaults):
+ if not hasattr(job, key):
+ replacements[key] = value
+
+ # Calculate the next run time if there is none defined
+ if not hasattr(job, 'next_run_time'):
+ now = datetime.now(self.timezone)
+ replacements['next_run_time'] = job.trigger.get_next_fire_time(None, now)
+
+ # Apply any replacements
+ job._modify(**replacements)
+
+ # Add the job to the given job store
+ store = self._lookup_jobstore(jobstore_alias)
+ try:
+ store.add_job(job)
+ except ConflictingIdError:
+ if replace_existing:
+ store.update_job(job)
+ else:
+ raise
+
+ # Mark the job as no longer pending
+ job._jobstore_alias = jobstore_alias
+
+ # Notify listeners that a new job has been added
+ event = JobEvent(EVENT_JOB_ADDED, job.id, jobstore_alias)
+ self._dispatch_event(event)
+
+ self._logger.info('Added job "%s" to job store "%s"', job.name, jobstore_alias)
+
+ # Notify the scheduler about the new job
+ if self.state == STATE_RUNNING:
+ self.wakeup()
+
+ def _create_plugin_instance(self, type_, alias, constructor_kwargs):
+ """Creates an instance of the given plugin type, loading the plugin first if necessary."""
+ plugin_container, class_container, base_class = {
+ 'trigger': (self._trigger_plugins, self._trigger_classes, BaseTrigger),
+ 'jobstore': (self._jobstore_plugins, self._jobstore_classes, BaseJobStore),
+ 'executor': (self._executor_plugins, self._executor_classes, BaseExecutor)
+ }[type_]
+
+ try:
+ plugin_cls = class_container[alias]
+ except KeyError:
+ if alias in plugin_container:
+ plugin_cls = class_container[alias] = plugin_container[alias].load()
+ if not issubclass(plugin_cls, base_class):
+ raise TypeError('The {0} entry point does not point to a {0} class'.
+ format(type_))
+ else:
+ raise LookupError('No {0} by the name "{1}" was found'.format(type_, alias))
+
+ return plugin_cls(**constructor_kwargs)
+
+ def _create_trigger(self, trigger, trigger_args):
+ if isinstance(trigger, BaseTrigger):
+ return trigger
+ elif trigger is None:
+ trigger = 'date'
+ elif not isinstance(trigger, six.string_types):
+ raise TypeError('Expected a trigger instance or string, got %s instead' %
+ trigger.__class__.__name__)
+
+ # Use the scheduler's time zone if nothing else is specified
+ trigger_args.setdefault('timezone', self.timezone)
+
+ # Instantiate the trigger class
+ return self._create_plugin_instance('trigger', trigger, trigger_args)
+
+ def _create_lock(self):
+ """Creates a reentrant lock object."""
+ return RLock()
+
+ def _process_jobs(self):
+ """
+ Iterates through jobs in every jobstore, starts jobs that are due and figures out how long
+ to wait for the next round.
+
+ If the ``get_due_jobs()`` call raises an exception, a new wakeup is scheduled in at least
+ ``jobstore_retry_interval`` seconds.
+
+ """
+ if self.state == STATE_PAUSED:
+ self._logger.debug('Scheduler is paused -- not processing jobs')
+ return None
+
+ self._logger.debug('Looking for jobs to run')
+ now = datetime.now(self.timezone)
+ next_wakeup_time = None
+ events = []
+
+ with self._jobstores_lock:
+ for jobstore_alias, jobstore in six.iteritems(self._jobstores):
+ try:
+ due_jobs = jobstore.get_due_jobs(now)
+ except Exception as e:
+ # Schedule a wakeup at least in jobstore_retry_interval seconds
+ self._logger.warning('Error getting due jobs from job store %r: %s',
+ jobstore_alias, e)
+ retry_wakeup_time = now + timedelta(seconds=self.jobstore_retry_interval)
+ if not next_wakeup_time or next_wakeup_time > retry_wakeup_time:
+ next_wakeup_time = retry_wakeup_time
+
+ continue
+
+ for job in due_jobs:
+ # Look up the job's executor
+ try:
+ executor = self._lookup_executor(job.executor)
+ except BaseException:
+ self._logger.error(
+ 'Executor lookup ("%s") failed for job "%s" -- removing it from the '
+ 'job store', job.executor, job)
+ self.remove_job(job.id, jobstore_alias)
+ continue
+
+ run_times = job._get_run_times(now)
+ run_times = run_times[-1:] if run_times and job.coalesce else run_times
+ if run_times:
+ try:
+ executor.submit_job(job, run_times)
+ except MaxInstancesReachedError:
+ self._logger.warning(
+ 'Execution of job "%s" skipped: maximum number of running '
+ 'instances reached (%d)', job, job.max_instances)
+ event = JobSubmissionEvent(EVENT_JOB_MAX_INSTANCES, job.id,
+ jobstore_alias, run_times)
+ events.append(event)
+ except BaseException:
+ self._logger.exception('Error submitting job "%s" to executor "%s"',
+ job, job.executor)
+ else:
+ event = JobSubmissionEvent(EVENT_JOB_SUBMITTED, job.id, jobstore_alias,
+ run_times)
+ events.append(event)
+
+ # Update the job if it has a next execution time.
+ # Otherwise remove it from the job store.
+ job_next_run = job.trigger.get_next_fire_time(run_times[-1], now)
+ if job_next_run:
+ job._modify(next_run_time=job_next_run)
+ jobstore.update_job(job)
+ else:
+ self.remove_job(job.id, jobstore_alias)
+
+ # Set a new next wakeup time if there isn't one yet or
+ # the jobstore has an even earlier one
+ jobstore_next_run_time = jobstore.get_next_run_time()
+ if jobstore_next_run_time and (next_wakeup_time is None or
+ jobstore_next_run_time < next_wakeup_time):
+ next_wakeup_time = jobstore_next_run_time.astimezone(self.timezone)
+
+ # Dispatch collected events
+ for event in events:
+ self._dispatch_event(event)
+
+ # Determine the delay until this method should be called again
+ if self.state == STATE_PAUSED:
+ wait_seconds = None
+ self._logger.debug('Scheduler is paused; waiting until resume() is called')
+ elif next_wakeup_time is None:
+ wait_seconds = None
+ self._logger.debug('No jobs; waiting until a job is added')
+ else:
+ wait_seconds = min(max(timedelta_seconds(next_wakeup_time - now), 0), TIMEOUT_MAX)
+ self._logger.debug('Next wakeup is due at %s (in %f seconds)', next_wakeup_time,
+ wait_seconds)
+
+ return wait_seconds
diff --git a/telegramer/include/apscheduler/schedulers/blocking.py b/telegramer/include/apscheduler/schedulers/blocking.py
new file mode 100644
index 0000000..4ecc9f6
--- /dev/null
+++ b/telegramer/include/apscheduler/schedulers/blocking.py
@@ -0,0 +1,35 @@
+from __future__ import absolute_import
+
+from threading import Event
+
+from apscheduler.schedulers.base import BaseScheduler, STATE_STOPPED
+from apscheduler.util import TIMEOUT_MAX
+
+
+class BlockingScheduler(BaseScheduler):
+ """
+ A scheduler that runs in the foreground
+ (:meth:`~apscheduler.schedulers.base.BaseScheduler.start` will block).
+ """
+ _event = None
+
+ def start(self, *args, **kwargs):
+ if self._event is None or self._event.is_set():
+ self._event = Event()
+
+ super(BlockingScheduler, self).start(*args, **kwargs)
+ self._main_loop()
+
+ def shutdown(self, wait=True):
+ super(BlockingScheduler, self).shutdown(wait)
+ self._event.set()
+
+ def _main_loop(self):
+ wait_seconds = TIMEOUT_MAX
+ while self.state != STATE_STOPPED:
+ self._event.wait(wait_seconds)
+ self._event.clear()
+ wait_seconds = self._process_jobs()
+
+ def wakeup(self):
+ self._event.set()
diff --git a/telegramer/include/apscheduler/schedulers/gevent.py b/telegramer/include/apscheduler/schedulers/gevent.py
new file mode 100644
index 0000000..d48ed74
--- /dev/null
+++ b/telegramer/include/apscheduler/schedulers/gevent.py
@@ -0,0 +1,35 @@
+from __future__ import absolute_import
+
+from apscheduler.schedulers.blocking import BlockingScheduler
+from apscheduler.schedulers.base import BaseScheduler
+
+try:
+ from gevent.event import Event
+ from gevent.lock import RLock
+ import gevent
+except ImportError: # pragma: nocover
+ raise ImportError('GeventScheduler requires gevent installed')
+
+
+class GeventScheduler(BlockingScheduler):
+ """A scheduler that runs as a Gevent greenlet."""
+
+ _greenlet = None
+
+ def start(self, *args, **kwargs):
+ self._event = Event()
+ BaseScheduler.start(self, *args, **kwargs)
+ self._greenlet = gevent.spawn(self._main_loop)
+ return self._greenlet
+
+ def shutdown(self, *args, **kwargs):
+ super(GeventScheduler, self).shutdown(*args, **kwargs)
+ self._greenlet.join()
+ del self._greenlet
+
+ def _create_lock(self):
+ return RLock()
+
+ def _create_default_executor(self):
+ from apscheduler.executors.gevent import GeventExecutor
+ return GeventExecutor()
diff --git a/telegramer/include/apscheduler/schedulers/qt.py b/telegramer/include/apscheduler/schedulers/qt.py
new file mode 100644
index 0000000..600f6e6
--- /dev/null
+++ b/telegramer/include/apscheduler/schedulers/qt.py
@@ -0,0 +1,50 @@
+from __future__ import absolute_import
+
+from apscheduler.schedulers.base import BaseScheduler
+
+try:
+ from PyQt5.QtCore import QObject, QTimer
+except (ImportError, RuntimeError): # pragma: nocover
+ try:
+ from PyQt4.QtCore import QObject, QTimer
+ except ImportError:
+ try:
+ from PySide6.QtCore import QObject, QTimer # noqa
+ except ImportError:
+ try:
+ from PySide2.QtCore import QObject, QTimer # noqa
+ except ImportError:
+ try:
+ from PySide.QtCore import QObject, QTimer # noqa
+ except ImportError:
+ raise ImportError('QtScheduler requires either PyQt5, PyQt4, PySide6, PySide2 '
+ 'or PySide installed')
+
+
+class QtScheduler(BaseScheduler):
+ """A scheduler that runs in a Qt event loop."""
+
+ _timer = None
+
+ def shutdown(self, *args, **kwargs):
+ super(QtScheduler, self).shutdown(*args, **kwargs)
+ self._stop_timer()
+
+ def _start_timer(self, wait_seconds):
+ self._stop_timer()
+ if wait_seconds is not None:
+ wait_time = min(wait_seconds * 1000, 2147483647)
+ self._timer = QTimer.singleShot(wait_time, self._process_jobs)
+
+ def _stop_timer(self):
+ if self._timer:
+ if self._timer.isActive():
+ self._timer.stop()
+ del self._timer
+
+ def wakeup(self):
+ self._start_timer(0)
+
+ def _process_jobs(self):
+ wait_seconds = super(QtScheduler, self)._process_jobs()
+ self._start_timer(wait_seconds)
diff --git a/telegramer/include/apscheduler/schedulers/tornado.py b/telegramer/include/apscheduler/schedulers/tornado.py
new file mode 100644
index 0000000..0a9171f
--- /dev/null
+++ b/telegramer/include/apscheduler/schedulers/tornado.py
@@ -0,0 +1,63 @@
+from __future__ import absolute_import
+
+from datetime import timedelta
+from functools import wraps
+
+from apscheduler.schedulers.base import BaseScheduler
+from apscheduler.util import maybe_ref
+
+try:
+ from tornado.ioloop import IOLoop
+except ImportError: # pragma: nocover
+ raise ImportError('TornadoScheduler requires tornado installed')
+
+
+def run_in_ioloop(func):
+ @wraps(func)
+ def wrapper(self, *args, **kwargs):
+ self._ioloop.add_callback(func, self, *args, **kwargs)
+ return wrapper
+
+
+class TornadoScheduler(BaseScheduler):
+ """
+ A scheduler that runs on a Tornado IOLoop.
+
+ The default executor can run jobs based on native coroutines (``async def``).
+
+ =========== ===============================================================
+ ``io_loop`` Tornado IOLoop instance to use (defaults to the global IO loop)
+ =========== ===============================================================
+ """
+
+ _ioloop = None
+ _timeout = None
+
+ @run_in_ioloop
+ def shutdown(self, wait=True):
+ super(TornadoScheduler, self).shutdown(wait)
+ self._stop_timer()
+
+ def _configure(self, config):
+ self._ioloop = maybe_ref(config.pop('io_loop', None)) or IOLoop.current()
+ super(TornadoScheduler, self)._configure(config)
+
+ def _start_timer(self, wait_seconds):
+ self._stop_timer()
+ if wait_seconds is not None:
+ self._timeout = self._ioloop.add_timeout(timedelta(seconds=wait_seconds), self.wakeup)
+
+ def _stop_timer(self):
+ if self._timeout:
+ self._ioloop.remove_timeout(self._timeout)
+ del self._timeout
+
+ def _create_default_executor(self):
+ from apscheduler.executors.tornado import TornadoExecutor
+ return TornadoExecutor()
+
+ @run_in_ioloop
+ def wakeup(self):
+ self._stop_timer()
+ wait_seconds = self._process_jobs()
+ self._start_timer(wait_seconds)
diff --git a/telegramer/include/apscheduler/schedulers/twisted.py b/telegramer/include/apscheduler/schedulers/twisted.py
new file mode 100644
index 0000000..6b43a84
--- /dev/null
+++ b/telegramer/include/apscheduler/schedulers/twisted.py
@@ -0,0 +1,62 @@
+from __future__ import absolute_import
+
+from functools import wraps
+
+from apscheduler.schedulers.base import BaseScheduler
+from apscheduler.util import maybe_ref
+
+try:
+ from twisted.internet import reactor as default_reactor
+except ImportError: # pragma: nocover
+ raise ImportError('TwistedScheduler requires Twisted installed')
+
+
+def run_in_reactor(func):
+ @wraps(func)
+ def wrapper(self, *args, **kwargs):
+ self._reactor.callFromThread(func, self, *args, **kwargs)
+ return wrapper
+
+
+class TwistedScheduler(BaseScheduler):
+ """
+ A scheduler that runs on a Twisted reactor.
+
+ Extra options:
+
+ =========== ========================================================
+ ``reactor`` Reactor instance to use (defaults to the global reactor)
+ =========== ========================================================
+ """
+
+ _reactor = None
+ _delayedcall = None
+
+ def _configure(self, config):
+ self._reactor = maybe_ref(config.pop('reactor', default_reactor))
+ super(TwistedScheduler, self)._configure(config)
+
+ @run_in_reactor
+ def shutdown(self, wait=True):
+ super(TwistedScheduler, self).shutdown(wait)
+ self._stop_timer()
+
+ def _start_timer(self, wait_seconds):
+ self._stop_timer()
+ if wait_seconds is not None:
+ self._delayedcall = self._reactor.callLater(wait_seconds, self.wakeup)
+
+ def _stop_timer(self):
+ if self._delayedcall and self._delayedcall.active():
+ self._delayedcall.cancel()
+ del self._delayedcall
+
+ @run_in_reactor
+ def wakeup(self):
+ self._stop_timer()
+ wait_seconds = self._process_jobs()
+ self._start_timer(wait_seconds)
+
+ def _create_default_executor(self):
+ from apscheduler.executors.twisted import TwistedExecutor
+ return TwistedExecutor()
diff --git a/telegramer/include/apscheduler/triggers/__init__.py b/telegramer/include/apscheduler/triggers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/telegramer/include/apscheduler/triggers/base.py b/telegramer/include/apscheduler/triggers/base.py
new file mode 100644
index 0000000..55d010d
--- /dev/null
+++ b/telegramer/include/apscheduler/triggers/base.py
@@ -0,0 +1,37 @@
+from abc import ABCMeta, abstractmethod
+from datetime import timedelta
+import random
+
+import six
+
+
+class BaseTrigger(six.with_metaclass(ABCMeta)):
+ """Abstract base class that defines the interface that every trigger must implement."""
+
+ __slots__ = ()
+
+ @abstractmethod
+ def get_next_fire_time(self, previous_fire_time, now):
+ """
+ Returns the next datetime to fire on, If no such datetime can be calculated, returns
+ ``None``.
+
+ :param datetime.datetime previous_fire_time: the previous time the trigger was fired
+ :param datetime.datetime now: current datetime
+ """
+
+ def _apply_jitter(self, next_fire_time, jitter, now):
+ """
+ Randomize ``next_fire_time`` by adding a random value (the jitter).
+
+ :param datetime.datetime|None next_fire_time: next fire time without jitter applied. If
+ ``None``, returns ``None``.
+ :param int|None jitter: maximum number of seconds to add to ``next_fire_time``
+ (if ``None`` or ``0``, returns ``next_fire_time``)
+ :param datetime.datetime now: current datetime
+ :return datetime.datetime|None: next fire time with a jitter.
+ """
+ if next_fire_time is None or not jitter:
+ return next_fire_time
+
+ return next_fire_time + timedelta(seconds=random.uniform(0, jitter))
diff --git a/telegramer/include/apscheduler/triggers/combining.py b/telegramer/include/apscheduler/triggers/combining.py
new file mode 100644
index 0000000..bb90006
--- /dev/null
+++ b/telegramer/include/apscheduler/triggers/combining.py
@@ -0,0 +1,95 @@
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.util import obj_to_ref, ref_to_obj
+
+
+class BaseCombiningTrigger(BaseTrigger):
+ __slots__ = ('triggers', 'jitter')
+
+ def __init__(self, triggers, jitter=None):
+ self.triggers = triggers
+ self.jitter = jitter
+
+ def __getstate__(self):
+ return {
+ 'version': 1,
+ 'triggers': [(obj_to_ref(trigger.__class__), trigger.__getstate__())
+ for trigger in self.triggers],
+ 'jitter': self.jitter
+ }
+
+ def __setstate__(self, state):
+ if state.get('version', 1) > 1:
+ raise ValueError(
+ 'Got serialized data for version %s of %s, but only versions up to 1 can be '
+ 'handled' % (state['version'], self.__class__.__name__))
+
+ self.jitter = state['jitter']
+ self.triggers = []
+ for clsref, state in state['triggers']:
+ cls = ref_to_obj(clsref)
+ trigger = cls.__new__(cls)
+ trigger.__setstate__(state)
+ self.triggers.append(trigger)
+
+ def __repr__(self):
+ return '<{}({}{})>'.format(self.__class__.__name__, self.triggers,
+ ', jitter={}'.format(self.jitter) if self.jitter else '')
+
+
+class AndTrigger(BaseCombiningTrigger):
+ """
+ Always returns the earliest next fire time that all the given triggers can agree on.
+ The trigger is considered to be finished when any of the given triggers has finished its
+ schedule.
+
+ Trigger alias: ``and``
+
+ :param list triggers: triggers to combine
+ :param int|None jitter: delay the job execution by ``jitter`` seconds at most
+ """
+
+ __slots__ = ()
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ while True:
+ fire_times = [trigger.get_next_fire_time(previous_fire_time, now)
+ for trigger in self.triggers]
+ if None in fire_times:
+ return None
+ elif min(fire_times) == max(fire_times):
+ return self._apply_jitter(fire_times[0], self.jitter, now)
+ else:
+ now = max(fire_times)
+
+ def __str__(self):
+ return 'and[{}]'.format(', '.join(str(trigger) for trigger in self.triggers))
+
+
+class OrTrigger(BaseCombiningTrigger):
+ """
+ Always returns the earliest next fire time produced by any of the given triggers.
+ The trigger is considered finished when all the given triggers have finished their schedules.
+
+ Trigger alias: ``or``
+
+ :param list triggers: triggers to combine
+ :param int|None jitter: delay the job execution by ``jitter`` seconds at most
+
+ .. note:: Triggers that depends on the previous fire time, such as the interval trigger, may
+ seem to behave strangely since they are always passed the previous fire time produced by
+ any of the given triggers.
+ """
+
+ __slots__ = ()
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ fire_times = [trigger.get_next_fire_time(previous_fire_time, now)
+ for trigger in self.triggers]
+ fire_times = [fire_time for fire_time in fire_times if fire_time is not None]
+ if fire_times:
+ return self._apply_jitter(min(fire_times), self.jitter, now)
+ else:
+ return None
+
+ def __str__(self):
+ return 'or[{}]'.format(', '.join(str(trigger) for trigger in self.triggers))
diff --git a/telegramer/include/apscheduler/triggers/cron/__init__.py b/telegramer/include/apscheduler/triggers/cron/__init__.py
new file mode 100644
index 0000000..b5389dd
--- /dev/null
+++ b/telegramer/include/apscheduler/triggers/cron/__init__.py
@@ -0,0 +1,239 @@
+from datetime import datetime, timedelta
+
+from tzlocal import get_localzone
+import six
+
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.triggers.cron.fields import (
+ BaseField, MonthField, WeekField, DayOfMonthField, DayOfWeekField, DEFAULT_VALUES)
+from apscheduler.util import (
+ datetime_ceil, convert_to_datetime, datetime_repr, astimezone, localize, normalize)
+
+
+class CronTrigger(BaseTrigger):
+ """
+ Triggers when current time matches all specified time constraints,
+ similarly to how the UNIX cron scheduler works.
+
+ :param int|str year: 4-digit year
+ :param int|str month: month (1-12)
+ :param int|str day: day of month (1-31)
+ :param int|str week: ISO week (1-53)
+ :param int|str day_of_week: number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun)
+ :param int|str hour: hour (0-23)
+ :param int|str minute: minute (0-59)
+ :param int|str second: second (0-59)
+ :param datetime|str start_date: earliest possible date/time to trigger on (inclusive)
+ :param datetime|str end_date: latest possible date/time to trigger on (inclusive)
+ :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations (defaults
+ to scheduler timezone)
+ :param int|None jitter: delay the job execution by ``jitter`` seconds at most
+
+ .. note:: The first weekday is always **monday**.
+ """
+
+ FIELD_NAMES = ('year', 'month', 'day', 'week', 'day_of_week', 'hour', 'minute', 'second')
+ FIELDS_MAP = {
+ 'year': BaseField,
+ 'month': MonthField,
+ 'week': WeekField,
+ 'day': DayOfMonthField,
+ 'day_of_week': DayOfWeekField,
+ 'hour': BaseField,
+ 'minute': BaseField,
+ 'second': BaseField
+ }
+
+ __slots__ = 'timezone', 'start_date', 'end_date', 'fields', 'jitter'
+
+ def __init__(self, year=None, month=None, day=None, week=None, day_of_week=None, hour=None,
+ minute=None, second=None, start_date=None, end_date=None, timezone=None,
+ jitter=None):
+ if timezone:
+ self.timezone = astimezone(timezone)
+ elif isinstance(start_date, datetime) and start_date.tzinfo:
+ self.timezone = start_date.tzinfo
+ elif isinstance(end_date, datetime) and end_date.tzinfo:
+ self.timezone = end_date.tzinfo
+ else:
+ self.timezone = get_localzone()
+
+ self.start_date = convert_to_datetime(start_date, self.timezone, 'start_date')
+ self.end_date = convert_to_datetime(end_date, self.timezone, 'end_date')
+
+ self.jitter = jitter
+
+ values = dict((key, value) for (key, value) in six.iteritems(locals())
+ if key in self.FIELD_NAMES and value is not None)
+ self.fields = []
+ assign_defaults = False
+ for field_name in self.FIELD_NAMES:
+ if field_name in values:
+ exprs = values.pop(field_name)
+ is_default = False
+ assign_defaults = not values
+ elif assign_defaults:
+ exprs = DEFAULT_VALUES[field_name]
+ is_default = True
+ else:
+ exprs = '*'
+ is_default = True
+
+ field_class = self.FIELDS_MAP[field_name]
+ field = field_class(field_name, exprs, is_default)
+ self.fields.append(field)
+
+ @classmethod
+ def from_crontab(cls, expr, timezone=None):
+ """
+ Create a :class:`~CronTrigger` from a standard crontab expression.
+
+ See https://en.wikipedia.org/wiki/Cron for more information on the format accepted here.
+
+ :param expr: minute, hour, day of month, month, day of week
+ :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations (
+ defaults to scheduler timezone)
+ :return: a :class:`~CronTrigger` instance
+
+ """
+ values = expr.split()
+ if len(values) != 5:
+ raise ValueError('Wrong number of fields; got {}, expected 5'.format(len(values)))
+
+ return cls(minute=values[0], hour=values[1], day=values[2], month=values[3],
+ day_of_week=values[4], timezone=timezone)
+
+ def _increment_field_value(self, dateval, fieldnum):
+ """
+ Increments the designated field and resets all less significant fields to their minimum
+ values.
+
+ :type dateval: datetime
+ :type fieldnum: int
+ :return: a tuple containing the new date, and the number of the field that was actually
+ incremented
+ :rtype: tuple
+ """
+
+ values = {}
+ i = 0
+ while i < len(self.fields):
+ field = self.fields[i]
+ if not field.REAL:
+ if i == fieldnum:
+ fieldnum -= 1
+ i -= 1
+ else:
+ i += 1
+ continue
+
+ if i < fieldnum:
+ values[field.name] = field.get_value(dateval)
+ i += 1
+ elif i > fieldnum:
+ values[field.name] = field.get_min(dateval)
+ i += 1
+ else:
+ value = field.get_value(dateval)
+ maxval = field.get_max(dateval)
+ if value == maxval:
+ fieldnum -= 1
+ i -= 1
+ else:
+ values[field.name] = value + 1
+ i += 1
+
+ difference = datetime(**values) - dateval.replace(tzinfo=None)
+ return normalize(dateval + difference), fieldnum
+
+ def _set_field_value(self, dateval, fieldnum, new_value):
+ values = {}
+ for i, field in enumerate(self.fields):
+ if field.REAL:
+ if i < fieldnum:
+ values[field.name] = field.get_value(dateval)
+ elif i > fieldnum:
+ values[field.name] = field.get_min(dateval)
+ else:
+ values[field.name] = new_value
+
+ return localize(datetime(**values), self.timezone)
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ if previous_fire_time:
+ start_date = min(now, previous_fire_time + timedelta(microseconds=1))
+ if start_date == previous_fire_time:
+ start_date += timedelta(microseconds=1)
+ else:
+ start_date = max(now, self.start_date) if self.start_date else now
+
+ fieldnum = 0
+ next_date = datetime_ceil(start_date).astimezone(self.timezone)
+ while 0 <= fieldnum < len(self.fields):
+ field = self.fields[fieldnum]
+ curr_value = field.get_value(next_date)
+ next_value = field.get_next_value(next_date)
+
+ if next_value is None:
+ # No valid value was found
+ next_date, fieldnum = self._increment_field_value(next_date, fieldnum - 1)
+ elif next_value > curr_value:
+ # A valid, but higher than the starting value, was found
+ if field.REAL:
+ next_date = self._set_field_value(next_date, fieldnum, next_value)
+ fieldnum += 1
+ else:
+ next_date, fieldnum = self._increment_field_value(next_date, fieldnum)
+ else:
+ # A valid value was found, no changes necessary
+ fieldnum += 1
+
+ # Return if the date has rolled past the end date
+ if self.end_date and next_date > self.end_date:
+ return None
+
+ if fieldnum >= 0:
+ next_date = self._apply_jitter(next_date, self.jitter, now)
+ return min(next_date, self.end_date) if self.end_date else next_date
+
+ def __getstate__(self):
+ return {
+ 'version': 2,
+ 'timezone': self.timezone,
+ 'start_date': self.start_date,
+ 'end_date': self.end_date,
+ 'fields': self.fields,
+ 'jitter': self.jitter,
+ }
+
+ def __setstate__(self, state):
+ # This is for compatibility with APScheduler 3.0.x
+ if isinstance(state, tuple):
+ state = state[1]
+
+ if state.get('version', 1) > 2:
+ raise ValueError(
+ 'Got serialized data for version %s of %s, but only versions up to 2 can be '
+ 'handled' % (state['version'], self.__class__.__name__))
+
+ self.timezone = state['timezone']
+ self.start_date = state['start_date']
+ self.end_date = state['end_date']
+ self.fields = state['fields']
+ self.jitter = state.get('jitter')
+
+ def __str__(self):
+ options = ["%s='%s'" % (f.name, f) for f in self.fields if not f.is_default]
+ return 'cron[%s]' % (', '.join(options))
+
+ def __repr__(self):
+ options = ["%s='%s'" % (f.name, f) for f in self.fields if not f.is_default]
+ if self.start_date:
+ options.append("start_date=%r" % datetime_repr(self.start_date))
+ if self.end_date:
+ options.append("end_date=%r" % datetime_repr(self.end_date))
+ if self.jitter:
+ options.append('jitter=%s' % self.jitter)
+
+ return "<%s (%s, timezone='%s')>" % (
+ self.__class__.__name__, ', '.join(options), self.timezone)
diff --git a/telegramer/include/apscheduler/triggers/cron/expressions.py b/telegramer/include/apscheduler/triggers/cron/expressions.py
new file mode 100644
index 0000000..55a3716
--- /dev/null
+++ b/telegramer/include/apscheduler/triggers/cron/expressions.py
@@ -0,0 +1,251 @@
+"""This module contains the expressions applicable for CronTrigger's fields."""
+
+from calendar import monthrange
+import re
+
+from apscheduler.util import asint
+
+__all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression',
+ 'WeekdayPositionExpression', 'LastDayOfMonthExpression')
+
+
+WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
+MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
+
+
+class AllExpression(object):
+ value_re = re.compile(r'\*(?:/(?P\d+))?$')
+
+ def __init__(self, step=None):
+ self.step = asint(step)
+ if self.step == 0:
+ raise ValueError('Increment must be higher than 0')
+
+ def validate_range(self, field_name):
+ from apscheduler.triggers.cron.fields import MIN_VALUES, MAX_VALUES
+
+ value_range = MAX_VALUES[field_name] - MIN_VALUES[field_name]
+ if self.step and self.step > value_range:
+ raise ValueError('the step value ({}) is higher than the total range of the '
+ 'expression ({})'.format(self.step, value_range))
+
+ def get_next_value(self, date, field):
+ start = field.get_value(date)
+ minval = field.get_min(date)
+ maxval = field.get_max(date)
+ start = max(start, minval)
+
+ if not self.step:
+ next = start
+ else:
+ distance_to_next = (self.step - (start - minval)) % self.step
+ next = start + distance_to_next
+
+ if next <= maxval:
+ return next
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.step == other.step
+
+ def __str__(self):
+ if self.step:
+ return '*/%d' % self.step
+ return '*'
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self.step)
+
+
+class RangeExpression(AllExpression):
+ value_re = re.compile(
+ r'(?P\d+)(?:-(?P\d+))?(?:/(?P\d+))?$')
+
+ def __init__(self, first, last=None, step=None):
+ super(RangeExpression, self).__init__(step)
+ first = asint(first)
+ last = asint(last)
+ if last is None and step is None:
+ last = first
+ if last is not None and first > last:
+ raise ValueError('The minimum value in a range must not be higher than the maximum')
+ self.first = first
+ self.last = last
+
+ def validate_range(self, field_name):
+ from apscheduler.triggers.cron.fields import MIN_VALUES, MAX_VALUES
+
+ super(RangeExpression, self).validate_range(field_name)
+ if self.first < MIN_VALUES[field_name]:
+ raise ValueError('the first value ({}) is lower than the minimum value ({})'
+ .format(self.first, MIN_VALUES[field_name]))
+ if self.last is not None and self.last > MAX_VALUES[field_name]:
+ raise ValueError('the last value ({}) is higher than the maximum value ({})'
+ .format(self.last, MAX_VALUES[field_name]))
+ value_range = (self.last or MAX_VALUES[field_name]) - self.first
+ if self.step and self.step > value_range:
+ raise ValueError('the step value ({}) is higher than the total range of the '
+ 'expression ({})'.format(self.step, value_range))
+
+ def get_next_value(self, date, field):
+ startval = field.get_value(date)
+ minval = field.get_min(date)
+ maxval = field.get_max(date)
+
+ # Apply range limits
+ minval = max(minval, self.first)
+ maxval = min(maxval, self.last) if self.last is not None else maxval
+ nextval = max(minval, startval)
+
+ # Apply the step if defined
+ if self.step:
+ distance_to_next = (self.step - (nextval - minval)) % self.step
+ nextval += distance_to_next
+
+ return nextval if nextval <= maxval else None
+
+ def __eq__(self, other):
+ return (isinstance(other, self.__class__) and self.first == other.first and
+ self.last == other.last)
+
+ def __str__(self):
+ if self.last != self.first and self.last is not None:
+ range = '%d-%d' % (self.first, self.last)
+ else:
+ range = str(self.first)
+
+ if self.step:
+ return '%s/%d' % (range, self.step)
+ return range
+
+ def __repr__(self):
+ args = [str(self.first)]
+ if self.last != self.first and self.last is not None or self.step:
+ args.append(str(self.last))
+ if self.step:
+ args.append(str(self.step))
+ return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
+
+
+class MonthRangeExpression(RangeExpression):
+ value_re = re.compile(r'(?P[a-z]+)(?:-(?P[a-z]+))?', re.IGNORECASE)
+
+ def __init__(self, first, last=None):
+ try:
+ first_num = MONTHS.index(first.lower()) + 1
+ except ValueError:
+ raise ValueError('Invalid month name "%s"' % first)
+
+ if last:
+ try:
+ last_num = MONTHS.index(last.lower()) + 1
+ except ValueError:
+ raise ValueError('Invalid month name "%s"' % last)
+ else:
+ last_num = None
+
+ super(MonthRangeExpression, self).__init__(first_num, last_num)
+
+ def __str__(self):
+ if self.last != self.first and self.last is not None:
+ return '%s-%s' % (MONTHS[self.first - 1], MONTHS[self.last - 1])
+ return MONTHS[self.first - 1]
+
+ def __repr__(self):
+ args = ["'%s'" % MONTHS[self.first]]
+ if self.last != self.first and self.last is not None:
+ args.append("'%s'" % MONTHS[self.last - 1])
+ return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
+
+
+class WeekdayRangeExpression(RangeExpression):
+ value_re = re.compile(r'(?P[a-z]+)(?:-(?P[a-z]+))?', re.IGNORECASE)
+
+ def __init__(self, first, last=None):
+ try:
+ first_num = WEEKDAYS.index(first.lower())
+ except ValueError:
+ raise ValueError('Invalid weekday name "%s"' % first)
+
+ if last:
+ try:
+ last_num = WEEKDAYS.index(last.lower())
+ except ValueError:
+ raise ValueError('Invalid weekday name "%s"' % last)
+ else:
+ last_num = None
+
+ super(WeekdayRangeExpression, self).__init__(first_num, last_num)
+
+ def __str__(self):
+ if self.last != self.first and self.last is not None:
+ return '%s-%s' % (WEEKDAYS[self.first], WEEKDAYS[self.last])
+ return WEEKDAYS[self.first]
+
+ def __repr__(self):
+ args = ["'%s'" % WEEKDAYS[self.first]]
+ if self.last != self.first and self.last is not None:
+ args.append("'%s'" % WEEKDAYS[self.last])
+ return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
+
+
+class WeekdayPositionExpression(AllExpression):
+ options = ['1st', '2nd', '3rd', '4th', '5th', 'last']
+ value_re = re.compile(r'(?P%s) +(?P(?:\d+|\w+))' %
+ '|'.join(options), re.IGNORECASE)
+
+ def __init__(self, option_name, weekday_name):
+ super(WeekdayPositionExpression, self).__init__(None)
+ try:
+ self.option_num = self.options.index(option_name.lower())
+ except ValueError:
+ raise ValueError('Invalid weekday position "%s"' % option_name)
+
+ try:
+ self.weekday = WEEKDAYS.index(weekday_name.lower())
+ except ValueError:
+ raise ValueError('Invalid weekday name "%s"' % weekday_name)
+
+ def get_next_value(self, date, field):
+ # Figure out the weekday of the month's first day and the number of days in that month
+ first_day_wday, last_day = monthrange(date.year, date.month)
+
+ # Calculate which day of the month is the first of the target weekdays
+ first_hit_day = self.weekday - first_day_wday + 1
+ if first_hit_day <= 0:
+ first_hit_day += 7
+
+ # Calculate what day of the month the target weekday would be
+ if self.option_num < 5:
+ target_day = first_hit_day + self.option_num * 7
+ else:
+ target_day = first_hit_day + ((last_day - first_hit_day) // 7) * 7
+
+ if target_day <= last_day and target_day >= date.day:
+ return target_day
+
+ def __eq__(self, other):
+ return (super(WeekdayPositionExpression, self).__eq__(other) and
+ self.option_num == other.option_num and self.weekday == other.weekday)
+
+ def __str__(self):
+ return '%s %s' % (self.options[self.option_num], WEEKDAYS[self.weekday])
+
+ def __repr__(self):
+ return "%s('%s', '%s')" % (self.__class__.__name__, self.options[self.option_num],
+ WEEKDAYS[self.weekday])
+
+
+class LastDayOfMonthExpression(AllExpression):
+ value_re = re.compile(r'last', re.IGNORECASE)
+
+ def __init__(self):
+ super(LastDayOfMonthExpression, self).__init__(None)
+
+ def get_next_value(self, date, field):
+ return monthrange(date.year, date.month)[1]
+
+ def __str__(self):
+ return 'last'
+
+ def __repr__(self):
+ return "%s()" % self.__class__.__name__
diff --git a/telegramer/include/apscheduler/triggers/cron/fields.py b/telegramer/include/apscheduler/triggers/cron/fields.py
new file mode 100644
index 0000000..86d620c
--- /dev/null
+++ b/telegramer/include/apscheduler/triggers/cron/fields.py
@@ -0,0 +1,111 @@
+"""Fields represent CronTrigger options which map to :class:`~datetime.datetime` fields."""
+
+from calendar import monthrange
+import re
+
+import six
+
+from apscheduler.triggers.cron.expressions import (
+ AllExpression, RangeExpression, WeekdayPositionExpression, LastDayOfMonthExpression,
+ WeekdayRangeExpression, MonthRangeExpression)
+
+
+__all__ = ('MIN_VALUES', 'MAX_VALUES', 'DEFAULT_VALUES', 'BaseField', 'WeekField',
+ 'DayOfMonthField', 'DayOfWeekField')
+
+
+MIN_VALUES = {'year': 1970, 'month': 1, 'day': 1, 'week': 1, 'day_of_week': 0, 'hour': 0,
+ 'minute': 0, 'second': 0}
+MAX_VALUES = {'year': 9999, 'month': 12, 'day': 31, 'week': 53, 'day_of_week': 6, 'hour': 23,
+ 'minute': 59, 'second': 59}
+DEFAULT_VALUES = {'year': '*', 'month': 1, 'day': 1, 'week': '*', 'day_of_week': '*', 'hour': 0,
+ 'minute': 0, 'second': 0}
+SEPARATOR = re.compile(' *, *')
+
+
+class BaseField(object):
+ REAL = True
+ COMPILERS = [AllExpression, RangeExpression]
+
+ def __init__(self, name, exprs, is_default=False):
+ self.name = name
+ self.is_default = is_default
+ self.compile_expressions(exprs)
+
+ def get_min(self, dateval):
+ return MIN_VALUES[self.name]
+
+ def get_max(self, dateval):
+ return MAX_VALUES[self.name]
+
+ def get_value(self, dateval):
+ return getattr(dateval, self.name)
+
+ def get_next_value(self, dateval):
+ smallest = None
+ for expr in self.expressions:
+ value = expr.get_next_value(dateval, self)
+ if smallest is None or (value is not None and value < smallest):
+ smallest = value
+
+ return smallest
+
+ def compile_expressions(self, exprs):
+ self.expressions = []
+
+ # Split a comma-separated expression list, if any
+ for expr in SEPARATOR.split(str(exprs).strip()):
+ self.compile_expression(expr)
+
+ def compile_expression(self, expr):
+ for compiler in self.COMPILERS:
+ match = compiler.value_re.match(expr)
+ if match:
+ compiled_expr = compiler(**match.groupdict())
+
+ try:
+ compiled_expr.validate_range(self.name)
+ except ValueError as e:
+ exc = ValueError('Error validating expression {!r}: {}'.format(expr, e))
+ six.raise_from(exc, None)
+
+ self.expressions.append(compiled_expr)
+ return
+
+ raise ValueError('Unrecognized expression "%s" for field "%s"' % (expr, self.name))
+
+ def __eq__(self, other):
+ return isinstance(self, self.__class__) and self.expressions == other.expressions
+
+ def __str__(self):
+ expr_strings = (str(e) for e in self.expressions)
+ return ','.join(expr_strings)
+
+ def __repr__(self):
+ return "%s('%s', '%s')" % (self.__class__.__name__, self.name, self)
+
+
+class WeekField(BaseField):
+ REAL = False
+
+ def get_value(self, dateval):
+ return dateval.isocalendar()[1]
+
+
+class DayOfMonthField(BaseField):
+ COMPILERS = BaseField.COMPILERS + [WeekdayPositionExpression, LastDayOfMonthExpression]
+
+ def get_max(self, dateval):
+ return monthrange(dateval.year, dateval.month)[1]
+
+
+class DayOfWeekField(BaseField):
+ REAL = False
+ COMPILERS = BaseField.COMPILERS + [WeekdayRangeExpression]
+
+ def get_value(self, dateval):
+ return dateval.weekday()
+
+
+class MonthField(BaseField):
+ COMPILERS = BaseField.COMPILERS + [MonthRangeExpression]
diff --git a/telegramer/include/apscheduler/triggers/date.py b/telegramer/include/apscheduler/triggers/date.py
new file mode 100644
index 0000000..0768100
--- /dev/null
+++ b/telegramer/include/apscheduler/triggers/date.py
@@ -0,0 +1,51 @@
+from datetime import datetime
+
+from tzlocal import get_localzone
+
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.util import convert_to_datetime, datetime_repr, astimezone
+
+
+class DateTrigger(BaseTrigger):
+ """
+ Triggers once on the given datetime. If ``run_date`` is left empty, current time is used.
+
+ :param datetime|str run_date: the date/time to run the job at
+ :param datetime.tzinfo|str timezone: time zone for ``run_date`` if it doesn't have one already
+ """
+
+ __slots__ = 'run_date'
+
+ def __init__(self, run_date=None, timezone=None):
+ timezone = astimezone(timezone) or get_localzone()
+ if run_date is not None:
+ self.run_date = convert_to_datetime(run_date, timezone, 'run_date')
+ else:
+ self.run_date = datetime.now(timezone)
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ return self.run_date if previous_fire_time is None else None
+
+ def __getstate__(self):
+ return {
+ 'version': 1,
+ 'run_date': self.run_date
+ }
+
+ def __setstate__(self, state):
+ # This is for compatibility with APScheduler 3.0.x
+ if isinstance(state, tuple):
+ state = state[1]
+
+ if state.get('version', 1) > 1:
+ raise ValueError(
+ 'Got serialized data for version %s of %s, but only version 1 can be handled' %
+ (state['version'], self.__class__.__name__))
+
+ self.run_date = state['run_date']
+
+ def __str__(self):
+ return 'date[%s]' % datetime_repr(self.run_date)
+
+ def __repr__(self):
+ return "<%s (run_date='%s')>" % (self.__class__.__name__, datetime_repr(self.run_date))
diff --git a/telegramer/include/apscheduler/triggers/interval.py b/telegramer/include/apscheduler/triggers/interval.py
new file mode 100644
index 0000000..b0e2dbd
--- /dev/null
+++ b/telegramer/include/apscheduler/triggers/interval.py
@@ -0,0 +1,108 @@
+from datetime import timedelta, datetime
+from math import ceil
+
+from tzlocal import get_localzone
+
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.util import (
+ convert_to_datetime, normalize, timedelta_seconds, datetime_repr,
+ astimezone)
+
+
+class IntervalTrigger(BaseTrigger):
+ """
+ Triggers on specified intervals, starting on ``start_date`` if specified, ``datetime.now()`` +
+ interval otherwise.
+
+ :param int weeks: number of weeks to wait
+ :param int days: number of days to wait
+ :param int hours: number of hours to wait
+ :param int minutes: number of minutes to wait
+ :param int seconds: number of seconds to wait
+ :param datetime|str start_date: starting point for the interval calculation
+ :param datetime|str end_date: latest possible date/time to trigger on
+ :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations
+ :param int|None jitter: delay the job execution by ``jitter`` seconds at most
+ """
+
+ __slots__ = 'timezone', 'start_date', 'end_date', 'interval', 'interval_length', 'jitter'
+
+ def __init__(self, weeks=0, days=0, hours=0, minutes=0, seconds=0, start_date=None,
+ end_date=None, timezone=None, jitter=None):
+ self.interval = timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes,
+ seconds=seconds)
+ self.interval_length = timedelta_seconds(self.interval)
+ if self.interval_length == 0:
+ self.interval = timedelta(seconds=1)
+ self.interval_length = 1
+
+ if timezone:
+ self.timezone = astimezone(timezone)
+ elif isinstance(start_date, datetime) and start_date.tzinfo:
+ self.timezone = start_date.tzinfo
+ elif isinstance(end_date, datetime) and end_date.tzinfo:
+ self.timezone = end_date.tzinfo
+ else:
+ self.timezone = get_localzone()
+
+ start_date = start_date or (datetime.now(self.timezone) + self.interval)
+ self.start_date = convert_to_datetime(start_date, self.timezone, 'start_date')
+ self.end_date = convert_to_datetime(end_date, self.timezone, 'end_date')
+
+ self.jitter = jitter
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ if previous_fire_time:
+ next_fire_time = previous_fire_time + self.interval
+ elif self.start_date > now:
+ next_fire_time = self.start_date
+ else:
+ timediff_seconds = timedelta_seconds(now - self.start_date)
+ next_interval_num = int(ceil(timediff_seconds / self.interval_length))
+ next_fire_time = self.start_date + self.interval * next_interval_num
+
+ if self.jitter is not None:
+ next_fire_time = self._apply_jitter(next_fire_time, self.jitter, now)
+
+ if not self.end_date or next_fire_time <= self.end_date:
+ return normalize(next_fire_time)
+
+ def __getstate__(self):
+ return {
+ 'version': 2,
+ 'timezone': self.timezone,
+ 'start_date': self.start_date,
+ 'end_date': self.end_date,
+ 'interval': self.interval,
+ 'jitter': self.jitter,
+ }
+
+ def __setstate__(self, state):
+ # This is for compatibility with APScheduler 3.0.x
+ if isinstance(state, tuple):
+ state = state[1]
+
+ if state.get('version', 1) > 2:
+ raise ValueError(
+ 'Got serialized data for version %s of %s, but only versions up to 2 can be '
+ 'handled' % (state['version'], self.__class__.__name__))
+
+ self.timezone = state['timezone']
+ self.start_date = state['start_date']
+ self.end_date = state['end_date']
+ self.interval = state['interval']
+ self.interval_length = timedelta_seconds(self.interval)
+ self.jitter = state.get('jitter')
+
+ def __str__(self):
+ return 'interval[%s]' % str(self.interval)
+
+ def __repr__(self):
+ options = ['interval=%r' % self.interval, 'start_date=%r' % datetime_repr(self.start_date)]
+ if self.end_date:
+ options.append("end_date=%r" % datetime_repr(self.end_date))
+ if self.jitter:
+ options.append('jitter=%s' % self.jitter)
+
+ return "<%s (%s, timezone='%s')>" % (
+ self.__class__.__name__, ', '.join(options), self.timezone)
diff --git a/telegramer/include/apscheduler/util.py b/telegramer/include/apscheduler/util.py
new file mode 100644
index 0000000..d929a48
--- /dev/null
+++ b/telegramer/include/apscheduler/util.py
@@ -0,0 +1,438 @@
+"""This module contains several handy functions primarily meant for internal use."""
+
+from __future__ import division
+
+from datetime import date, datetime, time, timedelta, tzinfo
+from calendar import timegm
+from functools import partial
+from inspect import isclass, ismethod
+import re
+import sys
+
+from pytz import timezone, utc, FixedOffset
+import six
+
+try:
+ from inspect import signature
+except ImportError: # pragma: nocover
+ from funcsigs import signature
+
+try:
+ from threading import TIMEOUT_MAX
+except ImportError:
+ TIMEOUT_MAX = 4294967 # Maximum value accepted by Event.wait() on Windows
+
+try:
+ from asyncio import iscoroutinefunction
+except ImportError:
+ try:
+ from trollius import iscoroutinefunction
+ except ImportError:
+ def iscoroutinefunction(func):
+ return False
+
+__all__ = ('asint', 'asbool', 'astimezone', 'convert_to_datetime', 'datetime_to_utc_timestamp',
+ 'utc_timestamp_to_datetime', 'timedelta_seconds', 'datetime_ceil', 'get_callable_name',
+ 'obj_to_ref', 'ref_to_obj', 'maybe_ref', 'repr_escape', 'check_callable_args',
+ 'normalize', 'localize', 'TIMEOUT_MAX')
+
+
+class _Undefined(object):
+ def __nonzero__(self):
+ return False
+
+ def __bool__(self):
+ return False
+
+ def __repr__(self):
+ return ''
+
+
+undefined = _Undefined() #: a unique object that only signifies that no value is defined
+
+
+def asint(text):
+ """
+ Safely converts a string to an integer, returning ``None`` if the string is ``None``.
+
+ :type text: str
+ :rtype: int
+
+ """
+ if text is not None:
+ return int(text)
+
+
+def asbool(obj):
+ """
+ Interprets an object as a boolean value.
+
+ :rtype: bool
+
+ """
+ if isinstance(obj, str):
+ obj = obj.strip().lower()
+ if obj in ('true', 'yes', 'on', 'y', 't', '1'):
+ return True
+ if obj in ('false', 'no', 'off', 'n', 'f', '0'):
+ return False
+ raise ValueError('Unable to interpret value "%s" as boolean' % obj)
+ return bool(obj)
+
+
+def astimezone(obj):
+ """
+ Interprets an object as a timezone.
+
+ :rtype: tzinfo
+
+ """
+ if isinstance(obj, six.string_types):
+ return timezone(obj)
+ if isinstance(obj, tzinfo):
+ if obj.tzname(None) == 'local':
+ raise ValueError(
+ 'Unable to determine the name of the local timezone -- you must explicitly '
+ 'specify the name of the local timezone. Please refrain from using timezones like '
+ 'EST to prevent problems with daylight saving time. Instead, use a locale based '
+ 'timezone name (such as Europe/Helsinki).')
+ return obj
+ if obj is not None:
+ raise TypeError('Expected tzinfo, got %s instead' % obj.__class__.__name__)
+
+
+_DATE_REGEX = re.compile(
+ r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})'
+ r'(?:[ T](?P\d{1,2}):(?P\d{1,2}):(?P\d{1,2})'
+ r'(?:\.(?P\d{1,6}))?'
+ r'(?PZ|[+-]\d\d:\d\d)?)?$')
+
+
+def convert_to_datetime(input, tz, arg_name):
+ """
+ Converts the given object to a timezone aware datetime object.
+
+ If a timezone aware datetime object is passed, it is returned unmodified.
+ If a native datetime object is passed, it is given the specified timezone.
+ If the input is a string, it is parsed as a datetime with the given timezone.
+
+ Date strings are accepted in three different forms: date only (Y-m-d), date with time
+ (Y-m-d H:M:S) or with date+time with microseconds (Y-m-d H:M:S.micro). Additionally you can
+ override the time zone by giving a specific offset in the format specified by ISO 8601:
+ Z (UTC), +HH:MM or -HH:MM.
+
+ :param str|datetime input: the datetime or string to convert to a timezone aware datetime
+ :param datetime.tzinfo tz: timezone to interpret ``input`` in
+ :param str arg_name: the name of the argument (used in an error message)
+ :rtype: datetime
+
+ """
+ if input is None:
+ return
+ elif isinstance(input, datetime):
+ datetime_ = input
+ elif isinstance(input, date):
+ datetime_ = datetime.combine(input, time())
+ elif isinstance(input, six.string_types):
+ m = _DATE_REGEX.match(input)
+ if not m:
+ raise ValueError('Invalid date string')
+
+ values = m.groupdict()
+ tzname = values.pop('timezone')
+ if tzname == 'Z':
+ tz = utc
+ elif tzname:
+ hours, minutes = (int(x) for x in tzname[1:].split(':'))
+ sign = 1 if tzname[0] == '+' else -1
+ tz = FixedOffset(sign * (hours * 60 + minutes))
+
+ values = {k: int(v or 0) for k, v in values.items()}
+ datetime_ = datetime(**values)
+ else:
+ raise TypeError('Unsupported type for %s: %s' % (arg_name, input.__class__.__name__))
+
+ if datetime_.tzinfo is not None:
+ return datetime_
+ if tz is None:
+ raise ValueError(
+ 'The "tz" argument must be specified if %s has no timezone information' % arg_name)
+ if isinstance(tz, six.string_types):
+ tz = timezone(tz)
+
+ return localize(datetime_, tz)
+
+
+def datetime_to_utc_timestamp(timeval):
+ """
+ Converts a datetime instance to a timestamp.
+
+ :type timeval: datetime
+ :rtype: float
+
+ """
+ if timeval is not None:
+ return timegm(timeval.utctimetuple()) + timeval.microsecond / 1000000
+
+
+def utc_timestamp_to_datetime(timestamp):
+ """
+ Converts the given timestamp to a datetime instance.
+
+ :type timestamp: float
+ :rtype: datetime
+
+ """
+ if timestamp is not None:
+ return datetime.fromtimestamp(timestamp, utc)
+
+
+def timedelta_seconds(delta):
+ """
+ Converts the given timedelta to seconds.
+
+ :type delta: timedelta
+ :rtype: float
+
+ """
+ return delta.days * 24 * 60 * 60 + delta.seconds + \
+ delta.microseconds / 1000000.0
+
+
+def datetime_ceil(dateval):
+ """
+ Rounds the given datetime object upwards.
+
+ :type dateval: datetime
+
+ """
+ if dateval.microsecond > 0:
+ return dateval + timedelta(seconds=1, microseconds=-dateval.microsecond)
+ return dateval
+
+
+def datetime_repr(dateval):
+ return dateval.strftime('%Y-%m-%d %H:%M:%S %Z') if dateval else 'None'
+
+
+def get_callable_name(func):
+ """
+ Returns the best available display name for the given function/callable.
+
+ :rtype: str
+
+ """
+ # the easy case (on Python 3.3+)
+ if hasattr(func, '__qualname__'):
+ return func.__qualname__
+
+ # class methods, bound and unbound methods
+ f_self = getattr(func, '__self__', None) or getattr(func, 'im_self', None)
+ if f_self and hasattr(func, '__name__'):
+ f_class = f_self if isclass(f_self) else f_self.__class__
+ else:
+ f_class = getattr(func, 'im_class', None)
+
+ if f_class and hasattr(func, '__name__'):
+ return '%s.%s' % (f_class.__name__, func.__name__)
+
+ # class or class instance
+ if hasattr(func, '__call__'):
+ # class
+ if hasattr(func, '__name__'):
+ return func.__name__
+
+ # instance of a class with a __call__ method
+ return func.__class__.__name__
+
+ raise TypeError('Unable to determine a name for %r -- maybe it is not a callable?' % func)
+
+
+def obj_to_ref(obj):
+ """
+ Returns the path to the given callable.
+
+ :rtype: str
+ :raises TypeError: if the given object is not callable
+ :raises ValueError: if the given object is a :class:`~functools.partial`, lambda or a nested
+ function
+
+ """
+ if isinstance(obj, partial):
+ raise ValueError('Cannot create a reference to a partial()')
+
+ name = get_callable_name(obj)
+ if '' in name:
+ raise ValueError('Cannot create a reference to a lambda')
+ if '' in name:
+ raise ValueError('Cannot create a reference to a nested function')
+
+ if ismethod(obj):
+ if hasattr(obj, 'im_self') and obj.im_self:
+ # bound method
+ module = obj.im_self.__module__
+ elif hasattr(obj, 'im_class') and obj.im_class:
+ # unbound method
+ module = obj.im_class.__module__
+ else:
+ module = obj.__module__
+ else:
+ module = obj.__module__
+ return '%s:%s' % (module, name)
+
+
+def ref_to_obj(ref):
+ """
+ Returns the object pointed to by ``ref``.
+
+ :type ref: str
+
+ """
+ if not isinstance(ref, six.string_types):
+ raise TypeError('References must be strings')
+ if ':' not in ref:
+ raise ValueError('Invalid reference')
+
+ modulename, rest = ref.split(':', 1)
+ try:
+ obj = __import__(modulename, fromlist=[rest])
+ except ImportError:
+ raise LookupError('Error resolving reference %s: could not import module' % ref)
+
+ try:
+ for name in rest.split('.'):
+ obj = getattr(obj, name)
+ return obj
+ except Exception:
+ raise LookupError('Error resolving reference %s: error looking up object' % ref)
+
+
+def maybe_ref(ref):
+ """
+ Returns the object that the given reference points to, if it is indeed a reference.
+ If it is not a reference, the object is returned as-is.
+
+ """
+ if not isinstance(ref, str):
+ return ref
+ return ref_to_obj(ref)
+
+
+if six.PY2:
+ def repr_escape(string):
+ if isinstance(string, six.text_type):
+ return string.encode('ascii', 'backslashreplace')
+ return string
+else:
+ def repr_escape(string):
+ return string
+
+
+def check_callable_args(func, args, kwargs):
+ """
+ Ensures that the given callable can be called with the given arguments.
+
+ :type args: tuple
+ :type kwargs: dict
+
+ """
+ pos_kwargs_conflicts = [] # parameters that have a match in both args and kwargs
+ positional_only_kwargs = [] # positional-only parameters that have a match in kwargs
+ unsatisfied_args = [] # parameters in signature that don't have a match in args or kwargs
+ unsatisfied_kwargs = [] # keyword-only arguments that don't have a match in kwargs
+ unmatched_args = list(args) # args that didn't match any of the parameters in the signature
+ # kwargs that didn't match any of the parameters in the signature
+ unmatched_kwargs = list(kwargs)
+ # indicates if the signature defines *args and **kwargs respectively
+ has_varargs = has_var_kwargs = False
+
+ try:
+ if sys.version_info >= (3, 5):
+ sig = signature(func, follow_wrapped=False)
+ else:
+ sig = signature(func)
+ except ValueError:
+ # signature() doesn't work against every kind of callable
+ return
+
+ for param in six.itervalues(sig.parameters):
+ if param.kind == param.POSITIONAL_OR_KEYWORD:
+ if param.name in unmatched_kwargs and unmatched_args:
+ pos_kwargs_conflicts.append(param.name)
+ elif unmatched_args:
+ del unmatched_args[0]
+ elif param.name in unmatched_kwargs:
+ unmatched_kwargs.remove(param.name)
+ elif param.default is param.empty:
+ unsatisfied_args.append(param.name)
+ elif param.kind == param.POSITIONAL_ONLY:
+ if unmatched_args:
+ del unmatched_args[0]
+ elif param.name in unmatched_kwargs:
+ unmatched_kwargs.remove(param.name)
+ positional_only_kwargs.append(param.name)
+ elif param.default is param.empty:
+ unsatisfied_args.append(param.name)
+ elif param.kind == param.KEYWORD_ONLY:
+ if param.name in unmatched_kwargs:
+ unmatched_kwargs.remove(param.name)
+ elif param.default is param.empty:
+ unsatisfied_kwargs.append(param.name)
+ elif param.kind == param.VAR_POSITIONAL:
+ has_varargs = True
+ elif param.kind == param.VAR_KEYWORD:
+ has_var_kwargs = True
+
+ # Make sure there are no conflicts between args and kwargs
+ if pos_kwargs_conflicts:
+ raise ValueError('The following arguments are supplied in both args and kwargs: %s' %
+ ', '.join(pos_kwargs_conflicts))
+
+ # Check if keyword arguments are being fed to positional-only parameters
+ if positional_only_kwargs:
+ raise ValueError('The following arguments cannot be given as keyword arguments: %s' %
+ ', '.join(positional_only_kwargs))
+
+ # Check that the number of positional arguments minus the number of matched kwargs matches the
+ # argspec
+ if unsatisfied_args:
+ raise ValueError('The following arguments have not been supplied: %s' %
+ ', '.join(unsatisfied_args))
+
+ # Check that all keyword-only arguments have been supplied
+ if unsatisfied_kwargs:
+ raise ValueError(
+ 'The following keyword-only arguments have not been supplied in kwargs: %s' %
+ ', '.join(unsatisfied_kwargs))
+
+ # Check that the callable can accept the given number of positional arguments
+ if not has_varargs and unmatched_args:
+ raise ValueError(
+ 'The list of positional arguments is longer than the target callable can handle '
+ '(allowed: %d, given in args: %d)' % (len(args) - len(unmatched_args), len(args)))
+
+ # Check that the callable can accept the given keyword arguments
+ if not has_var_kwargs and unmatched_kwargs:
+ raise ValueError(
+ 'The target callable does not accept the following keyword arguments: %s' %
+ ', '.join(unmatched_kwargs))
+
+
+def iscoroutinefunction_partial(f):
+ while isinstance(f, partial):
+ f = f.func
+
+ # The asyncio version of iscoroutinefunction includes testing for @coroutine
+ # decorations vs. the inspect version which does not.
+ return iscoroutinefunction(f)
+
+
+def normalize(dt):
+ return datetime.fromtimestamp(dt.timestamp(), dt.tzinfo)
+
+
+def localize(dt, tzinfo):
+ if hasattr(tzinfo, 'localize'):
+ return tzinfo.localize(dt)
+
+ return normalize(dt.replace(tzinfo=tzinfo))
diff --git a/telegramer/include/cacert/cacert.pem b/telegramer/include/cacert/cacert.pem
new file mode 100644
index 0000000..264923b
--- /dev/null
+++ b/telegramer/include/cacert/cacert.pem
@@ -0,0 +1,3138 @@
+##
+## Bundle of CA Root Certificates
+##
+## Certificate data from Mozilla as of: Tue May 25 03:12:05 2021 GMT
+##
+## This is a bundle of X.509 certificates of public Certificate Authorities
+## (CA). These were automatically extracted from Mozilla's root certificates
+## file (certdata.txt). This file can be found in the mozilla source tree:
+## https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt
+##
+## It contains the certificates in PEM format and therefore
+## can be directly used with curl / libcurl / php_curl, or with
+## an Apache+mod_ssl webserver for SSL client authentication.
+## Just configure this file as the SSLCACertificateFile.
+##
+## Conversion done with mk-ca-bundle.pl version 1.28.
+## SHA256: e292bd4e2d500c86df45b830d89417be5c42ee670408f1d2c454c63d8a782865
+##
+
+
+GlobalSign Root CA
+==================
+-----BEGIN CERTIFICATE-----
+MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx
+GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds
+b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV
+BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD
+VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa
+DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc
+THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb
+Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP
+c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX
+gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF
+AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj
+Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG
+j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH
+hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC
+X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
+-----END CERTIFICATE-----
+
+GlobalSign Root CA - R2
+=======================
+-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv
+YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh
+bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT
+aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln
+bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6
+ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp
+s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN
+S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL
+TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C
+ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
+FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i
+YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN
+BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp
+9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu
+01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7
+9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
+TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
+-----END CERTIFICATE-----
+
+Entrust.net Premium 2048 Secure Server CA
+=========================================
+-----BEGIN CERTIFICATE-----
+MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u
+ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp
+bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV
+BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx
+NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3
+d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl
+MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u
+ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL
+Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr
+hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW
+nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi
+VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ
+KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy
+T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf
+zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT
+J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e
+nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE=
+-----END CERTIFICATE-----
+
+Baltimore CyberTrust Root
+=========================
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE
+ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li
+ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC
+SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs
+dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME
+uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB
+UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C
+G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9
+XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr
+l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI
+VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB
+BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh
+cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5
+hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa
+Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H
+RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
+-----END CERTIFICATE-----
+
+Entrust Root Certification Authority
+====================================
+-----BEGIN CERTIFICATE-----
+MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV
+BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw
+b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG
+A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0
+MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu
+MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu
+Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v
+dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz
+A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww
+Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68
+j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN
+rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw
+DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1
+MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH
+hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA
+A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM
+Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa
+v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS
+W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0
+tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8
+-----END CERTIFICATE-----
+
+Comodo AAA Services root
+========================
+-----BEGIN CERTIFICATE-----
+MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS
+R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg
+TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw
+MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl
+c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
+BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG
+C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs
+i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW
+Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH
+Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK
+Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f
+BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl
+cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz
+LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm
+7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
+Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z
+8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C
+12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
+-----END CERTIFICATE-----
+
+QuoVadis Root CA
+================
+-----BEGIN CERTIFICATE-----
+MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE
+ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
+eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz
+MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp
+cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD
+EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk
+J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL
+F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL
+YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen
+AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w
+PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y
+ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7
+MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj
+YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs
+ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh
+Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW
+Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu
+BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw
+FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6
+tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo
+fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul
+LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x
+gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi
+5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi
+5nrQNiOKSnQ2+Q==
+-----END CERTIFICATE-----
+
+QuoVadis Root CA 2
+==================
+-----BEGIN CERTIFICATE-----
+MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT
+EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx
+ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6
+XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk
+lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB
+lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy
+lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt
+66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn
+wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh
+D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy
+BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie
+J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud
+DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU
+a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT
+ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv
+Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3
+UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm
+VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK
++JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW
+IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1
+WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X
+f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II
+4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8
+VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u
+-----END CERTIFICATE-----
+
+QuoVadis Root CA 3
+==================
+-----BEGIN CERTIFICATE-----
+MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT
+EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx
+OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg
+DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij
+KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K
+DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv
+BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp
+p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8
+nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX
+MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM
+Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz
+uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT
+BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj
+YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0
+aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB
+BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD
+VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4
+ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE
+AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV
+qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s
+hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z
+POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2
+Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp
+8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC
+bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu
+g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p
+vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr
+qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto=
+-----END CERTIFICATE-----
+
+Security Communication Root CA
+==============================
+-----BEGIN CERTIFICATE-----
+MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP
+U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw
+HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP
+U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw
+8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM
+DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX
+5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd
+DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2
+JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw
+DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g
+0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a
+mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ
+s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ
+6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi
+FL39vmwLAw==
+-----END CERTIFICATE-----
+
+Sonera Class 2 Root CA
+======================
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG
+U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw
+NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh
+IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3
+/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT
+dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG
+f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P
+tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH
+nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT
+XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt
+0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI
+cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph
+Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx
+EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH
+llpwrN9M
+-----END CERTIFICATE-----
+
+XRamp Global CA Root
+====================
+-----BEGIN CERTIFICATE-----
+MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE
+BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj
+dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB
+dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx
+HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg
+U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
+dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu
+IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx
+foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE
+zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs
+AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry
+xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
+EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap
+oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC
+AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc
+/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt
+qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n
+nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz
+8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw=
+-----END CERTIFICATE-----
+
+Go Daddy Class 2 CA
+===================
+-----BEGIN CERTIFICATE-----
+MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY
+VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG
+A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g
+RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD
+ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv
+2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32
+qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j
+YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY
+vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O
+BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o
+atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu
+MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG
+A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim
+PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt
+I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
+HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI
+Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b
+vZ8=
+-----END CERTIFICATE-----
+
+Starfield Class 2 CA
+====================
+-----BEGIN CERTIFICATE-----
+MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc
+U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo
+MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG
+A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG
+SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY
+bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ
+JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm
+epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN
+F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF
+MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f
+hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo
+bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g
+QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs
+afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM
+PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl
+xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD
+KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3
+QBFGmh95DmK/D5fs4C8fF5Q=
+-----END CERTIFICATE-----
+
+DigiCert Assured ID Root CA
+===========================
+-----BEGIN CERTIFICATE-----
+MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw
+IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx
+MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL
+ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO
+9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy
+UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW
+/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy
+oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf
+GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF
+66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq
+hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc
+EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn
+SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i
+8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe
++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==
+-----END CERTIFICATE-----
+
+DigiCert Global Root CA
+=======================
+-----BEGIN CERTIFICATE-----
+MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw
+HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw
+MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
+dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn
+TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5
+BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H
+4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y
+7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB
+o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm
+8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF
+BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr
+EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt
+tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886
+UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
+CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
+-----END CERTIFICATE-----
+
+DigiCert High Assurance EV Root CA
+==================================
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw
+KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw
+MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ
+MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu
+Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t
+Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS
+OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3
+MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ
+NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe
+h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB
+Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY
+JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ
+V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp
+myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK
+mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
+vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K
+-----END CERTIFICATE-----
+
+DST Root CA X3
+==============
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK
+ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X
+DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1
+cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT
+rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9
+UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy
+xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d
+utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T
+AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ
+MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug
+dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE
+GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw
+RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS
+fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
+-----END CERTIFICATE-----
+
+SwissSign Gold CA - G2
+======================
+-----BEGIN CERTIFICATE-----
+MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw
+EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN
+MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp
+c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq
+t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C
+jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg
+vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF
+ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR
+AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend
+jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO
+peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR
+7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi
+GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64
+OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov
+L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm
+5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr
+44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf
+Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m
+Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp
+mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk
+vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf
+KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br
+NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj
+viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ
+-----END CERTIFICATE-----
+
+SwissSign Silver CA - G2
+========================
+-----BEGIN CERTIFICATE-----
+MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT
+BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X
+DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3
+aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG
+9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644
+N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm
++/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH
+6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu
+MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h
+qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5
+FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs
+ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc
+celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X
+CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
+BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB
+tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0
+cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P
+4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F
+kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L
+3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx
+/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa
+DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP
+e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu
+WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ
+DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub
+DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u
+-----END CERTIFICATE-----
+
+SecureTrust CA
+==============
+-----BEGIN CERTIFICATE-----
+MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG
+EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy
+dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe
+BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX
+OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t
+DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH
+GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b
+01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH
+ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/
+BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj
+aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ
+KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu
+SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf
+mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ
+nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR
+3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=
+-----END CERTIFICATE-----
+
+Secure Global CA
+================
+-----BEGIN CERTIFICATE-----
+MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG
+EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH
+bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg
+MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg
+Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx
+YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ
+bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g
+8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV
+HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi
+0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
+EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn
+oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA
+MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+
+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn
+CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5
+3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc
+f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW
+-----END CERTIFICATE-----
+
+COMODO Certification Authority
+==============================
+-----BEGIN CERTIFICATE-----
+MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE
+BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG
+A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1
+dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb
+MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD
+T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH
++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww
+xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV
+4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA
+1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI
+rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k
+b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC
+AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP
+OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/
+RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc
+IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN
++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ==
+-----END CERTIFICATE-----
+
+Network Solutions Certificate Authority
+=======================================
+-----BEGIN CERTIFICATE-----
+MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG
+EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr
+IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx
+MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu
+MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx
+jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT
+aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT
+crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc
+/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB
+AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv
+bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA
+A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q
+4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/
+GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv
+wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD
+ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey
+-----END CERTIFICATE-----
+
+COMODO ECC Certification Authority
+==================================
+-----BEGIN CERTIFICATE-----
+MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC
+R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE
+ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB
+dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix
+GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo
+b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X
+4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni
+wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG
+FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA
+U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
+-----END CERTIFICATE-----
+
+Certigna
+========
+-----BEGIN CERTIFICATE-----
+MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw
+EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3
+MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI
+Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q
+XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH
+GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p
+ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg
+DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf
+Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ
+tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ
+BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J
+SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA
+hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+
+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu
+PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY
+1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
+WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
+-----END CERTIFICATE-----
+
+Cybertrust Global Root
+======================
+-----BEGIN CERTIFICATE-----
+MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li
+ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4
+MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD
+ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
++Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW
+0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL
+AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin
+89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT
+8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2
+MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G
+A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO
+lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi
+5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2
+hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T
+X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW
+WL1WMRJOEcgh4LMRkWXbtKaIOM5V
+-----END CERTIFICATE-----
+
+ePKI Root Certification Authority
+=================================
+-----BEGIN CERTIFICATE-----
+MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG
+EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg
+Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx
+MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq
+MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs
+IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi
+lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv
+qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX
+12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O
+WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+
+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao
+lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/
+vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi
+Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi
+MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH
+ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0
+1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq
+KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV
+xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP
+NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r
+GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE
+xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx
+gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy
+sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD
+BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw=
+-----END CERTIFICATE-----
+
+certSIGN ROOT CA
+================
+-----BEGIN CERTIFICATE-----
+MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD
+VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa
+Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE
+CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I
+JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH
+rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2
+ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD
+0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943
+AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B
+Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB
+AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8
+SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0
+x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt
+vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz
+TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD
+-----END CERTIFICATE-----
+
+NetLock Arany (Class Gold) Főtanúsítvány
+========================================
+-----BEGIN CERTIFICATE-----
+MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G
+A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610
+dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB
+cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx
+MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO
+ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv
+biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6
+c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu
+0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw
+/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk
+H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw
+fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1
+neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW
+qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta
+YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
+bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna
+NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu
+dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
+-----END CERTIFICATE-----
+
+Hongkong Post Root CA 1
+=======================
+-----BEGIN CERTIFICATE-----
+MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT
+DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx
+NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n
+IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1
+ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr
+auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh
+qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY
+V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV
+HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i
+h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio
+l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei
+IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps
+T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT
+c4afU9hDDl3WY4JxHYB0yvbiAmvZWg==
+-----END CERTIFICATE-----
+
+SecureSign RootCA11
+===================
+-----BEGIN CERTIFICATE-----
+MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi
+SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS
+b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw
+KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1
+cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL
+TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO
+wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq
+g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP
+O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA
+bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX
+t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh
+OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r
+bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ
+Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01
+y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061
+lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I=
+-----END CERTIFICATE-----
+
+Microsec e-Szigno Root CA 2009
+==============================
+-----BEGIN CERTIFICATE-----
+MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER
+MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv
+c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o
+dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE
+BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt
+U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA
+fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG
+0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA
+pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm
+1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC
+AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf
+QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE
+FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o
+lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX
+I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775
+tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02
+yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi
+LXpUq3DDfSJlgnCW
+-----END CERTIFICATE-----
+
+GlobalSign Root CA - R3
+=======================
+-----BEGIN CERTIFICATE-----
+MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv
+YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh
+bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT
+aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln
+bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt
+iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ
+0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3
+rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl
+OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2
+xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
+FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7
+lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8
+EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E
+bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18
+YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r
+kpeDMdmztcpHWD9f
+-----END CERTIFICATE-----
+
+Autoridad de Certificacion Firmaprofesional CIF A62634068
+=========================================================
+-----BEGIN CERTIFICATE-----
+MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA
+BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2
+MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw
+QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB
+NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD
+Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P
+B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY
+7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH
+ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI
+plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX
+MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX
+LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK
+bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU
+vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud
+EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH
+DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp
+cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA
+bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx
+ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx
+51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk
+R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP
+T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f
+Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl
+osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR
+crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR
+saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD
+KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi
+6Et8Vcad+qMUu2WFbm5PEn4KPJ2V
+-----END CERTIFICATE-----
+
+Izenpe.com
+==========
+-----BEGIN CERTIFICATE-----
+MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG
+EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz
+MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu
+QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ
+03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK
+ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU
++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC
+PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT
+OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK
+F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK
+0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+
+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB
+leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID
+AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+
+SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG
+NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx
+MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O
+BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l
+Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga
+kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q
+hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs
+g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5
+aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5
+nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC
+ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo
+Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z
+WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==
+-----END CERTIFICATE-----
+
+Go Daddy Root Certificate Authority - G2
+========================================
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT
+B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu
+MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5
+MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
+b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G
+A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq
+9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD
++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd
+fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl
+NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC
+MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9
+BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac
+vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r
+5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV
+N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
+LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1
+-----END CERTIFICATE-----
+
+Starfield Root Certificate Authority - G2
+=========================================
+-----BEGIN CERTIFICATE-----
+MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT
+B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s
+b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0
+eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw
+DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg
+VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB
+dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv
+W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs
+bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk
+N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf
+ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU
+JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol
+TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx
+4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw
+F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K
+pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ
+c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0
+-----END CERTIFICATE-----
+
+Starfield Services Root Certificate Authority - G2
+==================================================
+-----BEGIN CERTIFICATE-----
+MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT
+B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s
+b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl
+IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV
+BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT
+dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg
+Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2
+h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa
+hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP
+LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB
+rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG
+SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP
+E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy
+xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd
+iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza
+YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6
+-----END CERTIFICATE-----
+
+AffirmTrust Commercial
+======================
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS
+BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw
+MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly
+bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb
+DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV
+C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6
+BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww
+MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV
+HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG
+hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi
+qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv
+0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh
+sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=
+-----END CERTIFICATE-----
+
+AffirmTrust Networking
+======================
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS
+BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw
+MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly
+bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE
+Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI
+dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24
+/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb
+h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV
+HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu
+UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6
+12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23
+WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9
+/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=
+-----END CERTIFICATE-----
+
+AffirmTrust Premium
+===================
+-----BEGIN CERTIFICATE-----
+MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS
+BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy
+OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy
+dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn
+BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV
+5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs
++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd
+GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R
+p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI
+S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04
+6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5
+/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo
++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv
+MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg
+Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC
+6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S
+L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK
++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV
+BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg
+IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60
+g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb
+zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw==
+-----END CERTIFICATE-----
+
+AffirmTrust Premium ECC
+=======================
+-----BEGIN CERTIFICATE-----
+MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV
+BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx
+MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U
+cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA
+IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ
+N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW
+BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK
+BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X
+57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM
+eQ==
+-----END CERTIFICATE-----
+
+Certum Trusted Network CA
+=========================
+-----BEGIN CERTIFICATE-----
+MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
+ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy
+MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU
+ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC
+l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J
+J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4
+fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0
+cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB
+Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw
+DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj
+jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1
+mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj
+Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI
+03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=
+-----END CERTIFICATE-----
+
+TWCA Root Certification Authority
+=================================
+-----BEGIN CERTIFICATE-----
+MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ
+VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG
+EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB
+IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx
+QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC
+oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP
+4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r
+y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB
+BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG
+9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC
+mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW
+QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY
+T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny
+Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==
+-----END CERTIFICATE-----
+
+Security Communication RootCA2
+==============================
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc
+U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh
+dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC
+SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy
+aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++
++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R
+3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV
+spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K
+EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8
+QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB
+CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj
+u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk
+3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q
+tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29
+mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
+-----END CERTIFICATE-----
+
+EC-ACC
+======
+-----BEGIN CERTIFICATE-----
+MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE
+BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w
+ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD
+VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE
+CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT
+BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7
+MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt
+SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl
+Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh
+cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK
+w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT
+ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4
+HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a
+E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw
+0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E
+BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD
+VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0
+Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l
+dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ
+lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa
+Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe
+l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2
+E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D
+5EI=
+-----END CERTIFICATE-----
+
+Hellenic Academic and Research Institutions RootCA 2011
+=======================================================
+-----BEGIN CERTIFICATE-----
+MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT
+O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y
+aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z
+IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT
+AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z
+IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo
+IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI
+1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa
+71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u
+8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH
+3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/
+MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8
+MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu
+b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt
+XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8
+TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD
+/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N
+7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4
+-----END CERTIFICATE-----
+
+Actalis Authentication Root CA
+==============================
+-----BEGIN CERTIFICATE-----
+MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM
+BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE
+AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky
+MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz
+IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290
+IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ
+wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa
+by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6
+zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f
+YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2
+oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l
+EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7
+hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8
+EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5
+jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY
+iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt
+ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI
+WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0
+JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx
+K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+
+Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC
+4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo
+2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz
+lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem
+OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9
+vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
+-----END CERTIFICATE-----
+
+Trustis FPS Root CA
+===================
+-----BEGIN CERTIFICATE-----
+MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG
+EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290
+IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV
+BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ
+RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk
+H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa
+cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt
+o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA
+AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd
+BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c
+GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC
+yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P
+8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV
+l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl
+iB6XzCGcKQENZetX2fNXlrtIzYE=
+-----END CERTIFICATE-----
+
+Buypass Class 2 Root CA
+=======================
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
+QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X
+DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1
+eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1
+g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn
+9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b
+/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU
+CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff
+awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI
+zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn
+Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX
+Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs
+M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD
+VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF
+AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s
+A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI
+osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S
+aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd
+DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD
+LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0
+oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC
+wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS
+CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN
+rJgWVqA=
+-----END CERTIFICATE-----
+
+Buypass Class 3 Root CA
+=======================
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
+QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X
+DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1
+eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH
+sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR
+5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh
+7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ
+ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH
+2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV
+/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ
+RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA
+Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq
+j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD
+VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF
+AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV
+cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G
+uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG
+Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8
+ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2
+KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz
+6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug
+UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe
+eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi
+Cp/HuZc=
+-----END CERTIFICATE-----
+
+T-TeleSec GlobalRoot Class 3
+============================
+-----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM
+IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU
+cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx
+MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz
+dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD
+ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK
+9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU
+NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF
+iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W
+0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA
+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr
+AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb
+fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT
+ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h
+P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml
+e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw==
+-----END CERTIFICATE-----
+
+D-TRUST Root Class 3 CA 2 2009
+==============================
+-----BEGIN CERTIFICATE-----
+MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK
+DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe
+Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE
+LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD
+ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA
+BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv
+KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z
+p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC
+AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ
+4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y
+eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw
+MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G
+PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw
+OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm
+2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0
+o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV
+dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph
+X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I=
+-----END CERTIFICATE-----
+
+D-TRUST Root Class 3 CA 2 EV 2009
+=================================
+-----BEGIN CERTIFICATE-----
+MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK
+DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw
+OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK
+DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw
+OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS
+egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh
+zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T
+7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60
+sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35
+11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv
+cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v
+ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El
+MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp
+b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh
+c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+
+PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05
+nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX
+ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA
+NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv
+w9y4AyHqnxbxLFS1
+-----END CERTIFICATE-----
+
+CA Disig Root R2
+================
+-----BEGIN CERTIFICATE-----
+MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw
+EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp
+ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx
+EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp
+c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC
+w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia
+xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7
+A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S
+GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV
+g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa
+5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE
+koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A
+Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i
+Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV
+HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u
+Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM
+tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV
+sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je
+dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8
+1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx
+mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01
+utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0
+sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg
+UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV
+7+ZtsH8tZ/3zbBt1RqPlShfppNcL
+-----END CERTIFICATE-----
+
+ACCVRAIZ1
+=========
+-----BEGIN CERTIFICATE-----
+MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB
+SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1
+MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH
+UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM
+jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0
+RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD
+aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ
+0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG
+WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7
+8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR
+5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J
+9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK
+Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw
+Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu
+Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2
+VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM
+Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA
+QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh
+AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA
+YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj
+AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA
+IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk
+aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0
+dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2
+MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI
+hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E
+R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN
+YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49
+nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ
+TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3
+sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h
+I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg
+Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd
+3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p
+EfbRD0tVNEYqi4Y7
+-----END CERTIFICATE-----
+
+TWCA Global Root CA
+===================
+-----BEGIN CERTIFICATE-----
+MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT
+CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD
+QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK
+EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg
+Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C
+nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV
+r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR
+Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV
+tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W
+KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99
+sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p
+yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn
+kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI
+zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC
+AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g
+cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn
+LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M
+8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg
+/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg
+lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP
+A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m
+i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8
+EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3
+zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0=
+-----END CERTIFICATE-----
+
+TeliaSonera Root CA v1
+======================
+-----BEGIN CERTIFICATE-----
+MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE
+CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4
+MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW
+VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+
+6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA
+3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k
+B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn
+Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH
+oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3
+F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ
+oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7
+gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc
+TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB
+AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW
+DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm
+zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx
+0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW
+pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV
+G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc
+c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT
+JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2
+qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6
+Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems
+WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY=
+-----END CERTIFICATE-----
+
+E-Tugra Certification Authority
+===============================
+-----BEGIN CERTIFICATE-----
+MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNVBAYTAlRSMQ8w
+DQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamls
+ZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN
+ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMw
+NTEyMDk0OFoXDTIzMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmEx
+QDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxl
+cmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQD
+DB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEA4vU/kwVRHoViVF56C/UYB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vd
+hQd2h8y/L5VMzH2nPbxHD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5K
+CKpbknSFQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEoq1+g
+ElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3Dk14opz8n8Y4e0ypQ
+BaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcHfC425lAcP9tDJMW/hkd5s3kc91r0
+E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsutdEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gz
+rt48Ue7LE3wBf4QOXVGUnhMMti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAq
+jqFGOjGY5RH8zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn
+rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUXU8u3Zg5mTPj5
+dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6Jyr+zE7S6E5UMA8GA1UdEwEB
+/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEG
+MA0GCSqGSIb3DQEBCwUAA4ICAQAFNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAK
+kEh47U6YA5n+KGCRHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jO
+XKqYGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c77NCR807
+VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3+GbHeJAAFS6LrVE1Uweo
+a2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WKvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCc
+dlqMQ1DujjByTd//SffGqWfZbawCEeI6FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEV
+KV0jq9BgoRJP3vQXzTLlyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gT
+Dx4JnW2PAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpDy4Q0
+8ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8dNL/+I5c30jn6PQ0G
+C7TbO6Orb1wdtn7os4I07QZcJA==
+-----END CERTIFICATE-----
+
+T-TeleSec GlobalRoot Class 2
+============================
+-----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM
+IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU
+cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx
+MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz
+dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD
+ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ
+SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F
+vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970
+2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV
+WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA
+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy
+YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4
+r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf
+vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR
+3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN
+9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg==
+-----END CERTIFICATE-----
+
+Atos TrustedRoot 2011
+=====================
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU
+cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4
+MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG
+A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV
+hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr
+54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+
+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320
+HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR
+z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R
+l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ
+bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB
+CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h
+k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh
+TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9
+61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G
+3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed
+-----END CERTIFICATE-----
+
+QuoVadis Root CA 1 G3
+=====================
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG
+A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv
+b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN
+MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg
+RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE
+PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm
+PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6
+Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN
+ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l
+g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV
+7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX
+9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f
+iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg
+t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI
+hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC
+MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3
+GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct
+Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP
++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh
+3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa
+wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6
+O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0
+FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV
+hMJKzRwuJIczYOXD
+-----END CERTIFICATE-----
+
+QuoVadis Root CA 2 G3
+=====================
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG
+A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv
+b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN
+MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg
+RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh
+ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY
+NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t
+oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o
+MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l
+V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo
+L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ
+sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD
+6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh
+lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI
+hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66
+AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K
+pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9
+x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz
+dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X
+U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw
+mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD
+zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN
+JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr
+O3jtZsSOeWmD3n+M
+-----END CERTIFICATE-----
+
+QuoVadis Root CA 3 G3
+=====================
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG
+A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv
+b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN
+MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg
+RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286
+IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL
+Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe
+6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3
+I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U
+VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7
+5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi
+Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM
+dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt
+rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI
+hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px
+KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS
+t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ
+TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du
+DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib
+Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD
+hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX
+0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW
+dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2
+PpxxVJkES/1Y+Zj0
+-----END CERTIFICATE-----
+
+DigiCert Assured ID Root G2
+===========================
+-----BEGIN CERTIFICATE-----
+MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw
+IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw
+MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL
+ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH
+35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq
+bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw
+VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP
+YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn
+lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO
+w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv
+0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz
+d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW
+hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M
+jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo
+IhNzbM8m9Yop5w==
+-----END CERTIFICATE-----
+
+DigiCert Assured ID Root G3
+===========================
+-----BEGIN CERTIFICATE-----
+MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV
+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD
+VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1
+MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ
+BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb
+RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs
+KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF
+UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy
+YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy
+1vUhZscv6pZjamVFkpUBtA==
+-----END CERTIFICATE-----
+
+DigiCert Global Root G2
+=======================
+-----BEGIN CERTIFICATE-----
+MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw
+HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx
+MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
+dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ
+kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO
+3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV
+BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM
+UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB
+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu
+5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr
+F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U
+WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH
+QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/
+iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
+MrY=
+-----END CERTIFICATE-----
+
+DigiCert Global Root G3
+=======================
+-----BEGIN CERTIFICATE-----
+MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV
+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD
+VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw
+MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k
+aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C
+AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O
+YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp
+Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y
+3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34
+VOKa5Vt8sycX
+-----END CERTIFICATE-----
+
+DigiCert Trusted Root G4
+========================
+-----BEGIN CERTIFICATE-----
+MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw
+HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1
+MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G
+CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp
+pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o
+k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa
+vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY
+QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6
+MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm
+mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7
+f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH
+dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8
+oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud
+DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
+ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY
+ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr
+yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy
+7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah
+ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN
+5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb
+/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa
+5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK
+G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP
+82Z+
+-----END CERTIFICATE-----
+
+COMODO RSA Certification Authority
+==================================
+-----BEGIN CERTIFICATE-----
+MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE
+BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG
+A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC
+R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE
+ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB
+dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn
+dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ
+FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+
+5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG
+x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX
+2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL
+OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3
+sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C
+GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5
+WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E
+FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w
+DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt
+rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+
+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg
+tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW
+sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp
+pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA
+zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq
+ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52
+7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I
+LaZRfyHBNVOFBkpdn627G190
+-----END CERTIFICATE-----
+
+USERTrust RSA Certification Authority
+=====================================
+-----BEGIN CERTIFICATE-----
+MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK
+ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK
+ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz
+0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j
+Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn
+RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O
++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq
+/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE
+Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM
+lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8
+yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+
+eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
+BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
+MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW
+FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ
+7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ
+Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM
+8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi
+FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi
+yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c
+J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw
+sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx
+Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9
+-----END CERTIFICATE-----
+
+USERTrust ECC Certification Authority
+=====================================
+-----BEGIN CERTIFICATE-----
+MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
+aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
+aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2
+0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez
+nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV
+HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB
+HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu
+9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=
+-----END CERTIFICATE-----
+
+GlobalSign ECC Root CA - R4
+===========================
+-----BEGIN CERTIFICATE-----
+MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEkMCIGA1UECxMb
+R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD
+EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb
+R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD
+EwpHbG9iYWxTaWduMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprl
+OQcJFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAwDgYDVR0P
+AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61FuOJAf/sKbvu+M8k8o4TV
+MAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGXkPoUVy0D7O48027KqGx2vKLeuwIgJ6iF
+JzWbVsaj8kfSt24bAgAXqmemFZHe+pTsewv4n4Q=
+-----END CERTIFICATE-----
+
+GlobalSign ECC Root CA - R5
+===========================
+-----BEGIN CERTIFICATE-----
+MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb
+R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD
+EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb
+R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD
+EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6
+SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS
+h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd
+BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx
+uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7
+yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3
+-----END CERTIFICATE-----
+
+Staat der Nederlanden EV Root CA
+================================
+-----BEGIN CERTIFICATE-----
+MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJOTDEeMBwGA1UE
+CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFhdCBkZXIgTmVkZXJsYW5kZW4g
+RVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0yMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5M
+MR4wHAYDVQQKDBVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRl
+cmxhbmRlbiBFViBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkk
+SzrSM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nCUiY4iKTW
+O0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3dZ//BYY1jTw+bbRcwJu+r
+0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46prfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8
+Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13lpJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gV
+XJrm0w912fxBmJc+qiXbj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr
+08C+eKxCKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS/ZbV
+0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0XcgOPvZuM5l5Tnrmd
+74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH1vI4gnPah1vlPNOePqc7nvQDs/nx
+fRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrPpx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNC
+MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwa
+ivsnuL8wbqg7MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI
+eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u2dfOWBfoqSmu
+c0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHSv4ilf0X8rLiltTMMgsT7B/Zq
+5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTCwPTxGfARKbalGAKb12NMcIxHowNDXLldRqAN
+b/9Zjr7dn3LDWyvfjFvO5QxGbJKyCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tN
+f1zuacpzEPuKqf2evTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi
+5Dp6Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIaGl6I6lD4
+WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeLeG9QgkRQP2YGiqtDhFZK
+DyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGy
+eUN51q1veieQA6TqJIc/2b3Z6fJfUEkc7uzXLg==
+-----END CERTIFICATE-----
+
+IdenTrust Commercial Root CA 1
+==============================
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG
+EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS
+b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES
+MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB
+IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld
+hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/
+mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi
+1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C
+XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl
+3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy
+NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV
+WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg
+xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix
+uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC
+AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI
+hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH
+6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg
+ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt
+ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV
+YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX
+feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro
+kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe
+2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz
+Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R
+cGzM7vRX+Bi6hG6H
+-----END CERTIFICATE-----
+
+IdenTrust Public Sector Root CA 1
+=================================
+-----BEGIN CERTIFICATE-----
+MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG
+EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv
+ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV
+UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS
+b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy
+P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6
+Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI
+rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf
+qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS
+mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn
+ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh
+LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v
+iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL
+4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B
+Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw
+DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj
+t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A
+mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt
+GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt
+m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx
+NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4
+Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI
+ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC
+ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ
+3Wl9af0AVqW3rLatt8o+Ae+c
+-----END CERTIFICATE-----
+
+Entrust Root Certification Authority - G2
+=========================================
+-----BEGIN CERTIFICATE-----
+MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV
+BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy
+bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug
+b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw
+HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT
+DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx
+OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s
+eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP
+/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz
+HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU
+s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y
+TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx
+AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6
+0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z
+iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ
+Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi
+nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+
+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO
+e4pIb4tF9g==
+-----END CERTIFICATE-----
+
+Entrust Root Certification Authority - EC1
+==========================================
+-----BEGIN CERTIFICATE-----
+MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx
+FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn
+YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl
+ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw
+FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs
+LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg
+dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt
+IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy
+AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef
+9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
+FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h
+vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8
+kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G
+-----END CERTIFICATE-----
+
+CFCA EV ROOT
+============
+-----BEGIN CERTIFICATE-----
+MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE
+CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB
+IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw
+MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD
+DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV
+BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD
+7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN
+uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW
+ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7
+xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f
+py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K
+gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol
+hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ
+tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf
+BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB
+/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB
+ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q
+ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua
+4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG
+E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX
+BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn
+aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy
+PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX
+kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C
+ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
+-----END CERTIFICATE-----
+
+OISTE WISeKey Global Root GB CA
+===============================
+-----BEGIN CERTIFICATE-----
+MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG
+EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl
+ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw
+MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD
+VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds
+b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX
+scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP
+rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk
+9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o
+Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg
+GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB
+/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI
+hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD
+dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0
+VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui
+HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic
+Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM=
+-----END CERTIFICATE-----
+
+SZAFIR ROOT CA2
+===============
+-----BEGIN CERTIFICATE-----
+MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG
+A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV
+BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ
+BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD
+VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q
+qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK
+DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE
+2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ
+ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi
+ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P
+AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC
+AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5
+O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67
+oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul
+4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6
++/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw==
+-----END CERTIFICATE-----
+
+Certum Trusted Network CA 2
+===========================
+-----BEGIN CERTIFICATE-----
+MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE
+BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1
+bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y
+ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ
+TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl
+cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB
+IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9
+7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o
+CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b
+Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p
+uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130
+GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ
+9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB
+Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye
+hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM
+BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
+AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI
+hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW
+Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA
+L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo
+clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM
+pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb
+w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo
+J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm
+ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX
+is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7
+zAYspsbiDrW5viSP
+-----END CERTIFICATE-----
+
+Hellenic Academic and Research Institutions RootCA 2015
+=======================================================
+-----BEGIN CERTIFICATE-----
+MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT
+BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0
+aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl
+YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx
+MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg
+QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV
+BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw
+MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv
+bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh
+iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+
+6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd
+FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr
+i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F
+GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2
+fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu
+iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc
+Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI
+hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+
+D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM
+d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y
+d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn
+82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb
+davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F
+Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt
+J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa
+JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q
+p/UsQu0yrbYhnr68
+-----END CERTIFICATE-----
+
+Hellenic Academic and Research Institutions ECC RootCA 2015
+===========================================================
+-----BEGIN CERTIFICATE-----
+MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0
+aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u
+cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj
+aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw
+MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj
+IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD
+VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290
+Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP
+dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK
+Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O
+BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA
+GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn
+dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR
+-----END CERTIFICATE-----
+
+ISRG Root X1
+============
+-----BEGIN CERTIFICATE-----
+MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE
+BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD
+EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG
+EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT
+DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r
+Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1
+3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K
+b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN
+Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ
+4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf
+1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu
+hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH
+usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r
+OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G
+A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY
+9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
+ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV
+0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt
+hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw
+TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx
+e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA
+JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD
+YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n
+JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ
+m+kXQ99b21/+jh5Xos1AnX5iItreGCc=
+-----END CERTIFICATE-----
+
+AC RAIZ FNMT-RCM
+================
+-----BEGIN CERTIFICATE-----
+MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT
+AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw
+MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD
+TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
+ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf
+qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr
+btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL
+j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou
+08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw
+WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT
+tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ
+47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC
+ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa
+i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
+FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o
+dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD
+nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s
+D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ
+j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT
+Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW
++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7
+Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d
+8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm
+5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG
+rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM=
+-----END CERTIFICATE-----
+
+Amazon Root CA 1
+================
+-----BEGIN CERTIFICATE-----
+MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD
+VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1
+MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv
+bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH
+FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ
+gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t
+dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce
+VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB
+/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3
+DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM
+CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy
+8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa
+2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2
+xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5
+-----END CERTIFICATE-----
+
+Amazon Root CA 2
+================
+-----BEGIN CERTIFICATE-----
+MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD
+VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1
+MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv
+bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
+ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4
+kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp
+N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9
+AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd
+fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx
+kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS
+btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0
+Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN
+c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+
+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw
+DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA
+A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY
++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE
+YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW
+xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ
+gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW
+aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV
+Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3
+KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi
+JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw=
+-----END CERTIFICATE-----
+
+Amazon Root CA 3
+================
+-----BEGIN CERTIFICATE-----
+MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG
+EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy
+NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ
+MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB
+f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr
+Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43
+rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc
+eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw==
+-----END CERTIFICATE-----
+
+Amazon Root CA 4
+================
+-----BEGIN CERTIFICATE-----
+MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG
+EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy
+NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ
+MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN
+/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri
+83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA
+MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1
+AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA==
+-----END CERTIFICATE-----
+
+TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1
+=============================================
+-----BEGIN CERTIFICATE-----
+MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT
+D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr
+IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g
+TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp
+ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD
+VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt
+c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth
+bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11
+IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8
+6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc
+wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0
+3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9
+WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU
+ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ
+KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh
+AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc
+lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R
+e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j
+q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM=
+-----END CERTIFICATE-----
+
+GDCA TrustAUTH R5 ROOT
+======================
+-----BEGIN CERTIFICATE-----
+MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw
+BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD
+DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow
+YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ
+IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs
+AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p
+OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr
+pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ
+9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ
+xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM
+R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ
+D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4
+oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx
+9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR
+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg
+p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9
+H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35
+6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd
++PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ
+HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD
+F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ
+8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv
+/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT
+aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g==
+-----END CERTIFICATE-----
+
+TrustCor RootCert CA-1
+======================
+-----BEGIN CERTIFICATE-----
+MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYDVQQGEwJQQTEP
+MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig
+U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp
+dHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkx
+MjMxMTcyMzE2WjCBpDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFu
+YW1hIENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUGA1UECwwe
+VHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZUcnVzdENvciBSb290Q2Vy
+dCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv463leLCJhJrMxnHQFgKq1mq
+jQCj/IDHUHuO1CAmujIS2CNUSSUQIpidRtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4
+pQa81QBeCQryJ3pS/C3Vseq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0
+JEsq1pme9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CVEY4h
+gLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorWhnAbJN7+KIor0Gqw
+/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/DeOxCbeKyKsZn3MzUOcwHwYDVR0j
+BBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AYYwDQYJKoZIhvcNAQELBQADggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5
+mDo4Nvu7Zp5I/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf
+ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZyonnMlo2HD6C
+qFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djtsL1Ac59v2Z3kf9YKVmgenFK+P
+3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdNzl/HHk484IkzlQsPpTLWPFp5LBk=
+-----END CERTIFICATE-----
+
+TrustCor RootCert CA-2
+======================
+-----BEGIN CERTIFICATE-----
+MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNVBAYTAlBBMQ8w
+DQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQwIgYDVQQKDBtUcnVzdENvciBT
+eXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0
+eTEfMB0GA1UEAwwWVHJ1c3RDb3IgUm9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEy
+MzExNzI2MzlaMIGkMQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5h
+bWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U
+cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0
+IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnIG7CKqJiJJWQdsg4foDSq8Gb
+ZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9Nk
+RvRUqdw6VC0xK5mC8tkq1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1
+oYxOdqHp2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nKDOOb
+XUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hapeaz6LMvYHL1cEksr1
+/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF3wP+TfSvPd9cW436cOGlfifHhi5q
+jxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQP
+eSghYA2FFn3XVDjxklb9tTNMg9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+Ctg
+rKAmrhQhJ8Z3mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh
+8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAdBgNVHQ4EFgQU
+2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6UnrybPZx9mCAZ5YwwYrIwDwYD
+VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/h
+Osh80QA9z+LqBrWyOrsGS2h60COXdKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnp
+kpfbsEZC89NiqpX+MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv
+2wnL/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RXCI/hOWB3
+S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYaZH9bDTMJBzN7Bj8RpFxw
+PIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dv
+DDqPys/cA8GiCcjl/YBeyGBCARsaU1q7N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYU
+RpFHmygk71dSTlxCnKr3Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANE
+xdqtvArBAs8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp5KeX
+RKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu1uwJ
+-----END CERTIFICATE-----
+
+TrustCor ECA-1
+==============
+-----BEGIN CERTIFICATE-----
+MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYDVQQGEwJQQTEP
+MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig
+U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp
+dHkxFzAVBgNVBAMMDlRydXN0Q29yIEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3Mjgw
+N1owgZwxCzAJBgNVBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5
+MSQwIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29y
+IENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3IgRUNBLTEwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb3w9U73NjKYKtR8aja+3+XzP4Q1HpGjOR
+MRegdMTUpwHmspI+ap3tDvl0mEDTPwOABoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23
+xFUfJ3zSCNV2HykVh0A53ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmc
+p0yJF4OuowReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/wZ0+
+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZFZtS6mFjBAgMBAAGj
+YzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAfBgNVHSMEGDAWgBREnkj1zG1I1KBL
+f/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
+AAOCAQEABT41XBVwm8nHc2FvcivUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u
+/ukZMjgDfxT2AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F
+hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50soIipX1TH0Xs
+J5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BIWJZpTdwHjFGTot+fDz2LYLSC
+jaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1WitJ/X5g==
+-----END CERTIFICATE-----
+
+SSL.com Root Certification Authority RSA
+========================================
+-----BEGIN CERTIFICATE-----
+MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM
+BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x
+MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw
+MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx
+EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM
+LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C
+Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8
+P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge
+oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp
+k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z
+fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ
+gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2
+UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8
+1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s
+bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV
+HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE
+AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr
+dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf
+ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl
+u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq
+erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj
+MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ
+vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI
+Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y
+wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI
+WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k=
+-----END CERTIFICATE-----
+
+SSL.com Root Certification Authority ECC
+========================================
+-----BEGIN CERTIFICATE-----
+MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV
+BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv
+BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy
+MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO
+BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv
+bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA
+BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+
+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR
+hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT
+jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW
+e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z
+5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl
+-----END CERTIFICATE-----
+
+SSL.com EV Root Certification Authority RSA R2
+==============================================
+-----BEGIN CERTIFICATE-----
+MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w
+DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u
+MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy
+MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI
+DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD
+VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN
+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh
+hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w
+cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO
+Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+
+B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh
+CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim
+9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto
+RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm
+JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48
++qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV
+HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp
+qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1
+++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx
+Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G
+guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz
+OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7
+CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq
+lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR
+rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1
+hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX
+9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w==
+-----END CERTIFICATE-----
+
+SSL.com EV Root Certification Authority ECC
+===========================================
+-----BEGIN CERTIFICATE-----
+MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV
+BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy
+BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw
+MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx
+EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM
+LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB
+BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy
+3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O
+BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe
+5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ
+N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm
+m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg==
+-----END CERTIFICATE-----
+
+GlobalSign Root CA - R6
+=======================
+-----BEGIN CERTIFICATE-----
+MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX
+R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds
+b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i
+YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs
+U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss
+grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE
+3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF
+vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM
+PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+
+azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O
+WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy
+CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP
+0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN
+b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE
+AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV
+HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN
+nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0
+lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY
+BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym
+Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr
+3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1
+0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T
+uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK
+oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t
+JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA=
+-----END CERTIFICATE-----
+
+OISTE WISeKey Global Root GC CA
+===============================
+-----BEGIN CERTIFICATE-----
+MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD
+SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo
+MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa
+Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL
+ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh
+bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr
+VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab
+NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd
+BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E
+AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk
+AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9
+-----END CERTIFICATE-----
+
+GTS Root R1
+===========
+-----BEGIN CERTIFICATE-----
+MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG
+EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv
+b3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG
+A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx
+9vaMf/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7r
+aKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnW
+r4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqM
+LnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly
+4cpk9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr
+06zqkUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92
+wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om
+3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNu
+JLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEM
+BQADggIBADiWCu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1
+d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6ZXPYfcX3v73sv
+fuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZRgyFmxhE+885H7pwoHyXa/6xm
+ld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9b
+gsiG1eGZbYwE8na6SfZu6W0eX6DvJ4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq
+4BjFbkerQUIpm/ZgDdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWEr
+tXvM+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyyF62ARPBo
+pY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9SQ98POyDGCBDTtWTurQ0
+sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdwsE3PYJ/HQcu51OyLemGhmW/HGY0dVHLql
+CFF1pkgl
+-----END CERTIFICATE-----
+
+GTS Root R2
+===========
+-----BEGIN CERTIFICATE-----
+MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG
+EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv
+b3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG
+A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTuk
+k3LvCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo
+7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWI
+m8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5Gm
+dFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbu
+ak7MkogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscsz
+cTJGr61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW
+Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73Vululycsl
+aVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy
+5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEM
+BQADggIBALZp8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT
+vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiTz9D2PGcDFWEJ
++YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiApJiS4wGWAqoC7o87xdFtCjMw
+c3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvbpxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3Da
+WsYDQvTtN6LwG1BUSw7YhN4ZKJmBR64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5r
+n/WkhLx3+WuXrD5RRaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56Gtmwfu
+Nmsk0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC5AwiWVIQ
+7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiFizoHCBy69Y9Vmhh1fuXs
+gWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLnyOd/xCxgXS/Dr55FBcOEArf9LAhST4Ld
+o/DUhgkC
+-----END CERTIFICATE-----
+
+GTS Root R3
+===========
+-----BEGIN CERTIFICATE-----
+MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV
+UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg
+UjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE
+ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcq
+hkjOPQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUU
+Rout736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24Cej
+QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP
+0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFukfCPAlaUs3L6JbyO5o91lAFJekazInXJ0
+glMLfalAvWhgxeG4VDvBNhcl2MG9AjEAnjWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOa
+KaqW04MjyaR7YbPMAuhd
+-----END CERTIFICATE-----
+
+GTS Root R4
+===========
+-----BEGIN CERTIFICATE-----
+MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV
+UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg
+UjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE
+ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcq
+hkjOPQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa
+6zzuhXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqj
+QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV
+2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0CMRw3J5QdCHojXohw0+WbhXRIjVhLfoI
+N+4Zba3bssx9BzT1YBkstTTZbyACMANxsbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11x
+zPKwTdb+mciUqXWi4w==
+-----END CERTIFICATE-----
+
+UCA Global G2 Root
+==================
+-----BEGIN CERTIFICATE-----
+MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG
+EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x
+NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU
+cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT
+oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV
+8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS
+h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o
+LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/
+R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe
+KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa
+4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc
+OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97
+8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O
+BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo
+5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5
+1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A
+Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9
+yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX
+c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo
+jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk
+bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x
+ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn
+RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A==
+-----END CERTIFICATE-----
+
+UCA Extended Validation Root
+============================
+-----BEGIN CERTIFICATE-----
+MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG
+EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u
+IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G
+A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs
+iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF
+Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu
+eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR
+59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH
+0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR
+el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv
+B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth
+WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS
+NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS
+3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL
+BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR
+ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM
+aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4
+dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb
++7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW
+F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi
+GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc
+GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi
+djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr
+dhh2n1ax
+-----END CERTIFICATE-----
+
+Certigna Root CA
+================
+-----BEGIN CERTIFICATE-----
+MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE
+BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ
+MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda
+MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz
+MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX
+stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz
+KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8
+JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16
+XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq
+4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej
+wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ
+lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI
+jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/
+/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
+HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of
+1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy
+dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h
+LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl
+cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt
+OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP
+TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq
+7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3
+4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd
+8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS
+6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY
+tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS
+aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde
+E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=
+-----END CERTIFICATE-----
+
+emSign Root CA - G1
+===================
+-----BEGIN CERTIFICATE-----
+MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET
+MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
+ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx
+ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk
+aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN
+LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1
+cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW
+DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ
+6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH
+hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG
+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2
+vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q
+NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q
++Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih
+U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
+iN66zB+Afko=
+-----END CERTIFICATE-----
+
+emSign ECC Root CA - G3
+=======================
+-----BEGIN CERTIFICATE-----
+MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG
+A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
+MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4
+MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11
+ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g
+RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc
+58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr
+MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC
+AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D
+CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7
+jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj
+-----END CERTIFICATE-----
+
+emSign Root CA - C1
+===================
+-----BEGIN CERTIFICATE-----
+MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx
+EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp
+Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE
+BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD
+ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up
+ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/
+Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX
+OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V
+I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms
+lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+
+XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD
+ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp
+/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1
+NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9
+wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ
+BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI=
+-----END CERTIFICATE-----
+
+emSign ECC Root CA - C3
+=======================
+-----BEGIN CERTIFICATE-----
+MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG
+A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF
+Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE
+BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD
+ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd
+6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9
+SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA
+B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA
+MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU
+ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ==
+-----END CERTIFICATE-----
+
+Hongkong Post Root CA 3
+=======================
+-----BEGIN CERTIFICATE-----
+MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG
+A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK
+Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2
+MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv
+bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX
+SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz
+iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf
+jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim
+5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe
+sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj
+0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/
+JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u
+y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h
++bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG
+xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID
+AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e
+i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN
+AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw
+W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld
+y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov
++BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc
+eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw
+9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7
+nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY
+hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB
+60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq
+dBb9HxEGmpv0
+-----END CERTIFICATE-----
+
+Entrust Root Certification Authority - G4
+=========================================
+-----BEGIN CERTIFICATE-----
+MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV
+BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu
+bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1
+dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1
+dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT
+AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0
+L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv
+cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv
+cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D
+umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV
+3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds
+8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ
+e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7
+ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X
+xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV
+7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8
+dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW
+Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T
+AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n
+MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q
+jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht
+7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK
+YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt
+jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+
+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW
+RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA
+JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G
++TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT
+kcpG2om3PVODLAgfi49T3f+sHw==
+-----END CERTIFICATE-----
+
+Microsoft ECC Root Certificate Authority 2017
+=============================================
+-----BEGIN CERTIFICATE-----
+MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV
+UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND
+IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4
+MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw
+NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ
+BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6
+thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB
+eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM
++Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf
+Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR
+eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M=
+-----END CERTIFICATE-----
+
+Microsoft RSA Root Certificate Authority 2017
+=============================================
+-----BEGIN CERTIFICATE-----
+MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG
+EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg
+UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw
+NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u
+MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw
+ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml
+7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e
+S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7
+1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+
+dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F
+yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS
+MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr
+lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ
+0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ
+ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC
+NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og
+6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80
+dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk
++ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex
+/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy
+AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW
+ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE
+7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT
+c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D
+5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E
+-----END CERTIFICATE-----
+
+e-Szigno Root CA 2017
+=====================
+-----BEGIN CERTIFICATE-----
+MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw
+DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt
+MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa
+Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE
+CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp
+Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx
+s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G
+A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv
+vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA
+tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO
+svxyqltZ+efcMQ==
+-----END CERTIFICATE-----
+
+certSIGN Root CA G2
+===================
+-----BEGIN CERTIFICATE-----
+MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw
+EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy
+MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH
+TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05
+N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk
+abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg
+wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp
+dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh
+ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732
+jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf
+95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc
+z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL
+iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud
+DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB
+ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC
+b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB
+/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5
+8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5
+BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW
+atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU
+Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M
+NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N
+0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc=
+-----END CERTIFICATE-----
+
+Trustwave Global Certification Authority
+========================================
+-----BEGIN CERTIFICATE-----
+MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJV
+UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2
+ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u
+IEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJV
+UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2
+ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u
+IEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALldUShLPDeS0YLOvR29
+zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0XznswuvCAAJWX/NKSqIk4cXGIDtiLK0thAf
+LdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4Bq
+stTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9o
+WN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+
+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40
+Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcE
+uE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm
++9jaJXLE9gCxInm943xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqj
+ifLJS3tBEW1ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud
+EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIB
+BjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H
+PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0H
+ZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla
+4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5R
+vbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPTMaCm/zjd
+zyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qequ5AvzSxnI9O4fKSTx+O
+856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVyQYHTtgGJoC86cnn+OjC/QezH
+Yj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu
+3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP
+29FpHOTKyeC2nOnOcXHebD8WpHk=
+-----END CERTIFICATE-----
+
+Trustwave Global ECC P256 Certification Authority
+=================================================
+-----BEGIN CERTIFICATE-----
+MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJVUzER
+MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI
+b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYD
+VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy
+dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1
+NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH77bOYj
+43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoNFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqm
+P62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt
+0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjz
+RM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7
+-----END CERTIFICATE-----
+
+Trustwave Global ECC P384 Certification Authority
+=================================================
+-----BEGIN CERTIFICATE-----
+MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzER
+MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI
+b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYD
+VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy
+dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4
+NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGvaDXU1CDFH
+Ba5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr
+/TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNV
+HQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNn
+ADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGcl
+CrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw==
+-----END CERTIFICATE-----
+
+NAVER Global Root Certification Authority
+=========================================
+-----BEGIN CERTIFICATE-----
+MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTELMAkG
+A1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRGT1JNIENvcnAuMTIwMAYDVQQD
+DClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4
+NDJaFw0zNzA4MTgyMzU5NTlaMGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVT
+UyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVAiQqrDZBb
+UGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH38dq6SZeWYp34+hInDEW
++j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lEHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7
+XNr4rRVqmfeSVPc0W+m/6imBEtRTkZazkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2
+aacp+yPOiNgSnABIqKYPszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4
+Yb8ObtoqvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHfnZ3z
+VHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaGYQ5fG8Ir4ozVu53B
+A0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo0es+nPxdGoMuK8u180SdOqcXYZai
+cdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3aCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejy
+YhbLgGvtPe31HzClrkvJE+2KAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNV
+HQ4EFgQU0p+I36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB
+Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoNqo0hV4/GPnrK
+21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatjcu3cvuzHV+YwIHHW1xDBE1UB
+jCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bx
+hYTeodoS76TiEJd6eN4MUZeoIUCLhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTg
+E34h5prCy8VCZLQelHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTH
+D8z7p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8piKCk5XQ
+A76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLRLBT/DShycpWbXgnbiUSY
+qqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oG
+I/hGoiLtk/bdmuYqh7GYVPEi92tF4+KOdh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmg
+kpzNNIaRkPpkUZ3+/uul9XXeifdy
+-----END CERTIFICATE-----
+
+AC RAIZ FNMT-RCM SERVIDORES SEGUROS
+===================================
+-----BEGIN CERTIFICATE-----
+MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQswCQYDVQQGEwJF
+UzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgwFgYDVQRhDA9WQVRFUy1RMjgy
+NjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1SQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4
+MTIyMDA5MzczM1oXDTQzMTIyMDA5MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQt
+UkNNMQ4wDAYDVQQLDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNB
+QyBSQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuBBAAiA2IA
+BPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LHsbI6GA60XYyzZl2hNPk2
+LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oKUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqG
+SM49BAMDA2kAMGYCMQCuSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoD
+zBOQn5ICMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJyv+c=
+-----END CERTIFICATE-----
+
+GlobalSign Root R46
+===================
+-----BEGIN CERTIFICATE-----
+MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUAMEYxCzAJBgNV
+BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJv
+b3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAX
+BgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08Es
+CVeJOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQGvGIFAha/
+r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud316HCkD7rRlr+/fKYIje
+2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo0q3v84RLHIf8E6M6cqJaESvWJ3En7YEt
+bWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSEy132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvj
+K8Cd+RTyG/FWaha/LIWFzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD4
+12lPFzYE+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCNI/on
+ccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzsx2sZy/N78CsHpdls
+eVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqaByFrgY/bxFn63iLABJzjqls2k+g9
+vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYD
+VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEM
+BQADggIBAHx47PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg
+JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti2kM3S+LGteWy
+gxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIkpnnpHs6i58FZFZ8d4kuaPp92
+CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRFFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZm
+OUdkLG5NrmJ7v2B0GbhWrJKsFjLtrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qq
+JZ4d16GLuc1CLgSkZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwye
+qiv5u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP4vkYxboz
+nxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6N3ec592kD3ZDZopD8p/7
+DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3vouXsXgxT7PntgMTzlSdriVZzH81Xwj3
+QEUxeCp6
+-----END CERTIFICATE-----
+
+GlobalSign Root E46
+===================
+-----BEGIN CERTIFICATE-----
+MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYxCzAJBgNVBAYT
+AkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJvb3Qg
+RTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNV
+BAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcq
+hkjOPQIBBgUrgQQAIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkB
+jtjqR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGddyXqBPCCj
+QjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQxCpCPtsad0kRL
+gLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZk
+vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+
+CAezNIm8BZ/3Hobui3A=
+-----END CERTIFICATE-----
diff --git a/telegramer/include/cachetools/__init__.py b/telegramer/include/cachetools/__init__.py
new file mode 100644
index 0000000..03b6f25
--- /dev/null
+++ b/telegramer/include/cachetools/__init__.py
@@ -0,0 +1,718 @@
+"""Extensible memoizing collections and decorators."""
+
+__all__ = (
+ "Cache",
+ "FIFOCache",
+ "LFUCache",
+ "LRUCache",
+ "MRUCache",
+ "RRCache",
+ "TLRUCache",
+ "TTLCache",
+ "cached",
+ "cachedmethod",
+)
+
+__version__ = "5.0.0"
+
+import collections
+import collections.abc
+import functools
+import heapq
+import random
+import time
+
+from .keys import hashkey as _defaultkey
+
+
+def _methodkey(_, *args, **kwargs):
+ return _defaultkey(*args, **kwargs)
+
+
+class _DefaultSize:
+
+ __slots__ = ()
+
+ def __getitem__(self, _):
+ return 1
+
+ def __setitem__(self, _, value):
+ assert value == 1
+
+ def pop(self, _):
+ return 1
+
+
+class Cache(collections.abc.MutableMapping):
+ """Mutable mapping to serve as a simple cache or cache base class."""
+
+ __marker = object()
+
+ __size = _DefaultSize()
+
+ def __init__(self, maxsize, getsizeof=None):
+ if getsizeof:
+ self.getsizeof = getsizeof
+ if self.getsizeof is not Cache.getsizeof:
+ self.__size = dict()
+ self.__data = dict()
+ self.__currsize = 0
+ self.__maxsize = maxsize
+
+ def __repr__(self):
+ return "%s(%s, maxsize=%r, currsize=%r)" % (
+ self.__class__.__name__,
+ repr(self.__data),
+ self.__maxsize,
+ self.__currsize,
+ )
+
+ def __getitem__(self, key):
+ try:
+ return self.__data[key]
+ except KeyError:
+ return self.__missing__(key)
+
+ def __setitem__(self, key, value):
+ maxsize = self.__maxsize
+ size = self.getsizeof(value)
+ if size > maxsize:
+ raise ValueError("value too large")
+ if key not in self.__data or self.__size[key] < size:
+ while self.__currsize + size > maxsize:
+ self.popitem()
+ if key in self.__data:
+ diffsize = size - self.__size[key]
+ else:
+ diffsize = size
+ self.__data[key] = value
+ self.__size[key] = size
+ self.__currsize += diffsize
+
+ def __delitem__(self, key):
+ size = self.__size.pop(key)
+ del self.__data[key]
+ self.__currsize -= size
+
+ def __contains__(self, key):
+ return key in self.__data
+
+ def __missing__(self, key):
+ raise KeyError(key)
+
+ def __iter__(self):
+ return iter(self.__data)
+
+ def __len__(self):
+ return len(self.__data)
+
+ def get(self, key, default=None):
+ if key in self:
+ return self[key]
+ else:
+ return default
+
+ def pop(self, key, default=__marker):
+ if key in self:
+ value = self[key]
+ del self[key]
+ elif default is self.__marker:
+ raise KeyError(key)
+ else:
+ value = default
+ return value
+
+ def setdefault(self, key, default=None):
+ if key in self:
+ value = self[key]
+ else:
+ self[key] = value = default
+ return value
+
+ @property
+ def maxsize(self):
+ """The maximum size of the cache."""
+ return self.__maxsize
+
+ @property
+ def currsize(self):
+ """The current size of the cache."""
+ return self.__currsize
+
+ @staticmethod
+ def getsizeof(value):
+ """Return the size of a cache element's value."""
+ return 1
+
+
+class FIFOCache(Cache):
+ """First In First Out (FIFO) cache implementation."""
+
+ def __init__(self, maxsize, getsizeof=None):
+ Cache.__init__(self, maxsize, getsizeof)
+ self.__order = collections.OrderedDict()
+
+ def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+ cache_setitem(self, key, value)
+ try:
+ self.__order.move_to_end(key)
+ except KeyError:
+ self.__order[key] = None
+
+ def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+ cache_delitem(self, key)
+ del self.__order[key]
+
+ def popitem(self):
+ """Remove and return the `(key, value)` pair first inserted."""
+ try:
+ key = next(iter(self.__order))
+ except StopIteration:
+ raise KeyError("%s is empty" % type(self).__name__) from None
+ else:
+ return (key, self.pop(key))
+
+
+class LFUCache(Cache):
+ """Least Frequently Used (LFU) cache implementation."""
+
+ def __init__(self, maxsize, getsizeof=None):
+ Cache.__init__(self, maxsize, getsizeof)
+ self.__counter = collections.Counter()
+
+ def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+ value = cache_getitem(self, key)
+ if key in self: # __missing__ may not store item
+ self.__counter[key] -= 1
+ return value
+
+ def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+ cache_setitem(self, key, value)
+ self.__counter[key] -= 1
+
+ def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+ cache_delitem(self, key)
+ del self.__counter[key]
+
+ def popitem(self):
+ """Remove and return the `(key, value)` pair least frequently used."""
+ try:
+ ((key, _),) = self.__counter.most_common(1)
+ except ValueError:
+ raise KeyError("%s is empty" % type(self).__name__) from None
+ else:
+ return (key, self.pop(key))
+
+
+class LRUCache(Cache):
+ """Least Recently Used (LRU) cache implementation."""
+
+ def __init__(self, maxsize, getsizeof=None):
+ Cache.__init__(self, maxsize, getsizeof)
+ self.__order = collections.OrderedDict()
+
+ def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+ value = cache_getitem(self, key)
+ if key in self: # __missing__ may not store item
+ self.__update(key)
+ return value
+
+ def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+ cache_setitem(self, key, value)
+ self.__update(key)
+
+ def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+ cache_delitem(self, key)
+ del self.__order[key]
+
+ def popitem(self):
+ """Remove and return the `(key, value)` pair least recently used."""
+ try:
+ key = next(iter(self.__order))
+ except StopIteration:
+ raise KeyError("%s is empty" % type(self).__name__) from None
+ else:
+ return (key, self.pop(key))
+
+ def __update(self, key):
+ try:
+ self.__order.move_to_end(key)
+ except KeyError:
+ self.__order[key] = None
+
+
+class MRUCache(Cache):
+ """Most Recently Used (MRU) cache implementation."""
+
+ def __init__(self, maxsize, getsizeof=None):
+ Cache.__init__(self, maxsize, getsizeof)
+ self.__order = collections.OrderedDict()
+
+ def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+ value = cache_getitem(self, key)
+ if key in self: # __missing__ may not store item
+ self.__update(key)
+ return value
+
+ def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+ cache_setitem(self, key, value)
+ self.__update(key)
+
+ def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+ cache_delitem(self, key)
+ del self.__order[key]
+
+ def popitem(self):
+ """Remove and return the `(key, value)` pair most recently used."""
+ try:
+ key = next(iter(self.__order))
+ except StopIteration:
+ raise KeyError("%s is empty" % type(self).__name__) from None
+ else:
+ return (key, self.pop(key))
+
+ def __update(self, key):
+ try:
+ self.__order.move_to_end(key, last=False)
+ except KeyError:
+ self.__order[key] = None
+
+
+class RRCache(Cache):
+ """Random Replacement (RR) cache implementation."""
+
+ def __init__(self, maxsize, choice=random.choice, getsizeof=None):
+ Cache.__init__(self, maxsize, getsizeof)
+ self.__choice = choice
+
+ @property
+ def choice(self):
+ """The `choice` function used by the cache."""
+ return self.__choice
+
+ def popitem(self):
+ """Remove and return a random `(key, value)` pair."""
+ try:
+ key = self.__choice(list(self))
+ except IndexError:
+ raise KeyError("%s is empty" % type(self).__name__) from None
+ else:
+ return (key, self.pop(key))
+
+
+class _TimedCache(Cache):
+ """Base class for time aware cache implementations."""
+
+ class _Timer:
+ def __init__(self, timer):
+ self.__timer = timer
+ self.__nesting = 0
+
+ def __call__(self):
+ if self.__nesting == 0:
+ return self.__timer()
+ else:
+ return self.__time
+
+ def __enter__(self):
+ if self.__nesting == 0:
+ self.__time = time = self.__timer()
+ else:
+ time = self.__time
+ self.__nesting += 1
+ return time
+
+ def __exit__(self, *exc):
+ self.__nesting -= 1
+
+ def __reduce__(self):
+ return _TimedCache._Timer, (self.__timer,)
+
+ def __getattr__(self, name):
+ return getattr(self.__timer, name)
+
+ def __init__(self, maxsize, timer=time.monotonic, getsizeof=None):
+ Cache.__init__(self, maxsize, getsizeof)
+ self.__timer = _TimedCache._Timer(timer)
+
+ def __repr__(self, cache_repr=Cache.__repr__):
+ with self.__timer as time:
+ self.expire(time)
+ return cache_repr(self)
+
+ def __len__(self, cache_len=Cache.__len__):
+ with self.__timer as time:
+ self.expire(time)
+ return cache_len(self)
+
+ @property
+ def currsize(self):
+ with self.__timer as time:
+ self.expire(time)
+ return super().currsize
+
+ @property
+ def timer(self):
+ """The timer function used by the cache."""
+ return self.__timer
+
+ def clear(self):
+ with self.__timer as time:
+ self.expire(time)
+ Cache.clear(self)
+
+ def get(self, *args, **kwargs):
+ with self.__timer:
+ return Cache.get(self, *args, **kwargs)
+
+ def pop(self, *args, **kwargs):
+ with self.__timer:
+ return Cache.pop(self, *args, **kwargs)
+
+ def setdefault(self, *args, **kwargs):
+ with self.__timer:
+ return Cache.setdefault(self, *args, **kwargs)
+
+
+class TTLCache(_TimedCache):
+ """LRU Cache implementation with per-item time-to-live (TTL) value."""
+
+ class _Link:
+
+ __slots__ = ("key", "expires", "next", "prev")
+
+ def __init__(self, key=None, expires=None):
+ self.key = key
+ self.expires = expires
+
+ def __reduce__(self):
+ return TTLCache._Link, (self.key, self.expires)
+
+ def unlink(self):
+ next = self.next
+ prev = self.prev
+ prev.next = next
+ next.prev = prev
+
+ def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None):
+ _TimedCache.__init__(self, maxsize, timer, getsizeof)
+ self.__root = root = TTLCache._Link()
+ root.prev = root.next = root
+ self.__links = collections.OrderedDict()
+ self.__ttl = ttl
+
+ def __contains__(self, key):
+ try:
+ link = self.__links[key] # no reordering
+ except KeyError:
+ return False
+ else:
+ return self.timer() < link.expires
+
+ def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+ try:
+ link = self.__getlink(key)
+ except KeyError:
+ expired = False
+ else:
+ expired = not (self.timer() < link.expires)
+ if expired:
+ return self.__missing__(key)
+ else:
+ return cache_getitem(self, key)
+
+ def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+ with self.timer as time:
+ self.expire(time)
+ cache_setitem(self, key, value)
+ try:
+ link = self.__getlink(key)
+ except KeyError:
+ self.__links[key] = link = TTLCache._Link(key)
+ else:
+ link.unlink()
+ link.expires = time + self.__ttl
+ link.next = root = self.__root
+ link.prev = prev = root.prev
+ prev.next = root.prev = link
+
+ def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+ cache_delitem(self, key)
+ link = self.__links.pop(key)
+ link.unlink()
+ if not (self.timer() < link.expires):
+ raise KeyError(key)
+
+ def __iter__(self):
+ root = self.__root
+ curr = root.next
+ while curr is not root:
+ # "freeze" time for iterator access
+ with self.timer as time:
+ if time < curr.expires:
+ yield curr.key
+ curr = curr.next
+
+ def __setstate__(self, state):
+ self.__dict__.update(state)
+ root = self.__root
+ root.prev = root.next = root
+ for link in sorted(self.__links.values(), key=lambda obj: obj.expires):
+ link.next = root
+ link.prev = prev = root.prev
+ prev.next = root.prev = link
+ self.expire(self.timer())
+
+ @property
+ def ttl(self):
+ """The time-to-live value of the cache's items."""
+ return self.__ttl
+
+ def expire(self, time=None):
+ """Remove expired items from the cache."""
+ if time is None:
+ time = self.timer()
+ root = self.__root
+ curr = root.next
+ links = self.__links
+ cache_delitem = Cache.__delitem__
+ while curr is not root and not (time < curr.expires):
+ cache_delitem(self, curr.key)
+ del links[curr.key]
+ next = curr.next
+ curr.unlink()
+ curr = next
+
+ def popitem(self):
+ """Remove and return the `(key, value)` pair least recently used that
+ has not already expired.
+
+ """
+ with self.timer as time:
+ self.expire(time)
+ try:
+ key = next(iter(self.__links))
+ except StopIteration:
+ raise KeyError("%s is empty" % type(self).__name__) from None
+ else:
+ return (key, self.pop(key))
+
+ def __getlink(self, key):
+ value = self.__links[key]
+ self.__links.move_to_end(key)
+ return value
+
+
+class TLRUCache(_TimedCache):
+ """Time aware Least Recently Used (TLRU) cache implementation."""
+
+ @functools.total_ordering
+ class _Item:
+
+ __slots__ = ("key", "expires", "removed")
+
+ def __init__(self, key=None, expires=None):
+ self.key = key
+ self.expires = expires
+ self.removed = False
+
+ def __lt__(self, other):
+ return self.expires < other.expires
+
+ def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None):
+ _TimedCache.__init__(self, maxsize, timer, getsizeof)
+ self.__items = collections.OrderedDict()
+ self.__order = []
+ self.__ttu = ttu
+
+ def __contains__(self, key):
+ try:
+ item = self.__items[key] # no reordering
+ except KeyError:
+ return False
+ else:
+ return self.timer() < item.expires
+
+ def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+ try:
+ item = self.__getitem(key)
+ except KeyError:
+ expired = False
+ else:
+ expired = not (self.timer() < item.expires)
+ if expired:
+ return self.__missing__(key)
+ else:
+ return cache_getitem(self, key)
+
+ def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+ with self.timer as time:
+ expires = self.__ttu(key, value, time)
+ if not (time < expires):
+ return # skip expired items
+ self.expire(time)
+ cache_setitem(self, key, value)
+ # removing an existing item would break the heap structure, so
+ # only mark it as removed for now
+ try:
+ self.__getitem(key).removed = True
+ except KeyError:
+ pass
+ self.__items[key] = item = TLRUCache._Item(key, expires)
+ heapq.heappush(self.__order, item)
+
+ def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+ with self.timer as time:
+ # no self.expire() for performance reasons, e.g. self.clear() [#67]
+ cache_delitem(self, key)
+ item = self.__items.pop(key)
+ item.removed = True
+ if not (time < item.expires):
+ raise KeyError(key)
+
+ def __iter__(self):
+ for curr in self.__order:
+ # "freeze" time for iterator access
+ with self.timer as time:
+ if time < curr.expires and not curr.removed:
+ yield curr.key
+
+ @property
+ def ttu(self):
+ """The local time-to-use function used by the cache."""
+ return self.__ttu
+
+ def expire(self, time=None):
+ """Remove expired items from the cache."""
+ if time is None:
+ time = self.timer()
+ items = self.__items
+ order = self.__order
+ # clean up the heap if too many items are marked as removed
+ if len(order) > len(items) * 2:
+ self.__order = order = [item for item in order if not item.removed]
+ heapq.heapify(order)
+ cache_delitem = Cache.__delitem__
+ while order and (order[0].removed or not (time < order[0].expires)):
+ item = heapq.heappop(order)
+ if not item.removed:
+ cache_delitem(self, item.key)
+ del items[item.key]
+
+ def popitem(self):
+ """Remove and return the `(key, value)` pair least recently used that
+ has not already expired.
+
+ """
+ with self.timer as time:
+ self.expire(time)
+ try:
+ key = next(iter(self.__items))
+ except StopIteration:
+ raise KeyError("%s is empty" % self.__class__.__name__) from None
+ else:
+ return (key, self.pop(key))
+
+ def __getitem(self, key):
+ value = self.__items[key]
+ self.__items.move_to_end(key)
+ return value
+
+
+def cached(cache, key=_defaultkey, lock=None):
+ """Decorator to wrap a function with a memoizing callable that saves
+ results in a cache.
+
+ """
+
+ def decorator(func):
+ if cache is None:
+
+ def wrapper(*args, **kwargs):
+ return func(*args, **kwargs)
+
+ elif lock is None:
+
+ def wrapper(*args, **kwargs):
+ k = key(*args, **kwargs)
+ try:
+ return cache[k]
+ except KeyError:
+ pass # key not found
+ v = func(*args, **kwargs)
+ try:
+ cache[k] = v
+ except ValueError:
+ pass # value too large
+ return v
+
+ else:
+
+ def wrapper(*args, **kwargs):
+ k = key(*args, **kwargs)
+ try:
+ with lock:
+ return cache[k]
+ except KeyError:
+ pass # key not found
+ v = func(*args, **kwargs)
+ # in case of a race, prefer the item already in the cache
+ try:
+ with lock:
+ return cache.setdefault(k, v)
+ except ValueError:
+ return v # value too large
+
+ return functools.update_wrapper(wrapper, func)
+
+ return decorator
+
+
+def cachedmethod(cache, key=_methodkey, lock=None):
+ """Decorator to wrap a class or instance method with a memoizing
+ callable that saves results in a cache.
+
+ """
+
+ def decorator(method):
+ if lock is None:
+
+ def wrapper(self, *args, **kwargs):
+ c = cache(self)
+ if c is None:
+ return method(self, *args, **kwargs)
+ k = key(self, *args, **kwargs)
+ try:
+ return c[k]
+ except KeyError:
+ pass # key not found
+ v = method(self, *args, **kwargs)
+ try:
+ c[k] = v
+ except ValueError:
+ pass # value too large
+ return v
+
+ else:
+
+ def wrapper(self, *args, **kwargs):
+ c = cache(self)
+ if c is None:
+ return method(self, *args, **kwargs)
+ k = key(self, *args, **kwargs)
+ try:
+ with lock(self):
+ return c[k]
+ except KeyError:
+ pass # key not found
+ v = method(self, *args, **kwargs)
+ # in case of a race, prefer the item already in the cache
+ try:
+ with lock(self):
+ return c.setdefault(k, v)
+ except ValueError:
+ return v # value too large
+
+ return functools.update_wrapper(wrapper, method)
+
+ return decorator
diff --git a/telegramer/include/cachetools/func.py b/telegramer/include/cachetools/func.py
new file mode 100644
index 0000000..01702c2
--- /dev/null
+++ b/telegramer/include/cachetools/func.py
@@ -0,0 +1,171 @@
+"""`functools.lru_cache` compatible memoizing function decorators."""
+
+__all__ = ("fifo_cache", "lfu_cache", "lru_cache", "mru_cache", "rr_cache", "ttl_cache")
+
+import collections
+import functools
+import math
+import random
+import time
+
+try:
+ from threading import RLock
+except ImportError: # pragma: no cover
+ from dummy_threading import RLock
+
+from . import FIFOCache, LFUCache, LRUCache, MRUCache, RRCache, TTLCache
+from . import keys
+
+
+_CacheInfo = collections.namedtuple(
+ "CacheInfo", ["hits", "misses", "maxsize", "currsize"]
+)
+
+
+class _UnboundCache(dict):
+ @property
+ def maxsize(self):
+ return None
+
+ @property
+ def currsize(self):
+ return len(self)
+
+
+class _UnboundTTLCache(TTLCache):
+ def __init__(self, ttl, timer):
+ TTLCache.__init__(self, math.inf, ttl, timer)
+
+ @property
+ def maxsize(self):
+ return None
+
+
+def _cache(cache, typed):
+ maxsize = cache.maxsize
+
+ def decorator(func):
+ key = keys.typedkey if typed else keys.hashkey
+ lock = RLock()
+ stats = [0, 0]
+
+ def wrapper(*args, **kwargs):
+ k = key(*args, **kwargs)
+ with lock:
+ try:
+ v = cache[k]
+ stats[0] += 1
+ return v
+ except KeyError:
+ stats[1] += 1
+ v = func(*args, **kwargs)
+ # in case of a race, prefer the item already in the cache
+ try:
+ with lock:
+ return cache.setdefault(k, v)
+ except ValueError:
+ return v # value too large
+
+ def cache_info():
+ with lock:
+ hits, misses = stats
+ maxsize = cache.maxsize
+ currsize = cache.currsize
+ return _CacheInfo(hits, misses, maxsize, currsize)
+
+ def cache_clear():
+ with lock:
+ try:
+ cache.clear()
+ finally:
+ stats[:] = [0, 0]
+
+ wrapper.cache_info = cache_info
+ wrapper.cache_clear = cache_clear
+ wrapper.cache_parameters = lambda: {"maxsize": maxsize, "typed": typed}
+ functools.update_wrapper(wrapper, func)
+ return wrapper
+
+ return decorator
+
+
+def fifo_cache(maxsize=128, typed=False):
+ """Decorator to wrap a function with a memoizing callable that saves
+ up to `maxsize` results based on a First In First Out (FIFO)
+ algorithm.
+
+ """
+ if maxsize is None:
+ return _cache(_UnboundCache(), typed)
+ elif callable(maxsize):
+ return _cache(FIFOCache(128), typed)(maxsize)
+ else:
+ return _cache(FIFOCache(maxsize), typed)
+
+
+def lfu_cache(maxsize=128, typed=False):
+ """Decorator to wrap a function with a memoizing callable that saves
+ up to `maxsize` results based on a Least Frequently Used (LFU)
+ algorithm.
+
+ """
+ if maxsize is None:
+ return _cache(_UnboundCache(), typed)
+ elif callable(maxsize):
+ return _cache(LFUCache(128), typed)(maxsize)
+ else:
+ return _cache(LFUCache(maxsize), typed)
+
+
+def lru_cache(maxsize=128, typed=False):
+ """Decorator to wrap a function with a memoizing callable that saves
+ up to `maxsize` results based on a Least Recently Used (LRU)
+ algorithm.
+
+ """
+ if maxsize is None:
+ return _cache(_UnboundCache(), typed)
+ elif callable(maxsize):
+ return _cache(LRUCache(128), typed)(maxsize)
+ else:
+ return _cache(LRUCache(maxsize), typed)
+
+
+def mru_cache(maxsize=128, typed=False):
+ """Decorator to wrap a function with a memoizing callable that saves
+ up to `maxsize` results based on a Most Recently Used (MRU)
+ algorithm.
+ """
+ if maxsize is None:
+ return _cache(_UnboundCache(), typed)
+ elif callable(maxsize):
+ return _cache(MRUCache(128), typed)(maxsize)
+ else:
+ return _cache(MRUCache(maxsize), typed)
+
+
+def rr_cache(maxsize=128, choice=random.choice, typed=False):
+ """Decorator to wrap a function with a memoizing callable that saves
+ up to `maxsize` results based on a Random Replacement (RR)
+ algorithm.
+
+ """
+ if maxsize is None:
+ return _cache(_UnboundCache(), typed)
+ elif callable(maxsize):
+ return _cache(RRCache(128, choice), typed)(maxsize)
+ else:
+ return _cache(RRCache(maxsize, choice), typed)
+
+
+def ttl_cache(maxsize=128, ttl=600, timer=time.monotonic, typed=False):
+ """Decorator to wrap a function with a memoizing callable that saves
+ up to `maxsize` results based on a Least Recently Used (LRU)
+ algorithm with a per-item time-to-live (TTL) value.
+ """
+ if maxsize is None:
+ return _cache(_UnboundTTLCache(ttl, timer), typed)
+ elif callable(maxsize):
+ return _cache(TTLCache(128, ttl, timer), typed)(maxsize)
+ else:
+ return _cache(TTLCache(maxsize, ttl, timer), typed)
diff --git a/telegramer/include/cachetools/keys.py b/telegramer/include/cachetools/keys.py
new file mode 100644
index 0000000..13630a4
--- /dev/null
+++ b/telegramer/include/cachetools/keys.py
@@ -0,0 +1,52 @@
+"""Key functions for memoizing decorators."""
+
+__all__ = ("hashkey", "typedkey")
+
+
+class _HashedTuple(tuple):
+ """A tuple that ensures that hash() will be called no more than once
+ per element, since cache decorators will hash the key multiple
+ times on a cache miss. See also _HashedSeq in the standard
+ library functools implementation.
+
+ """
+
+ __hashvalue = None
+
+ def __hash__(self, hash=tuple.__hash__):
+ hashvalue = self.__hashvalue
+ if hashvalue is None:
+ self.__hashvalue = hashvalue = hash(self)
+ return hashvalue
+
+ def __add__(self, other, add=tuple.__add__):
+ return _HashedTuple(add(self, other))
+
+ def __radd__(self, other, add=tuple.__add__):
+ return _HashedTuple(add(other, self))
+
+ def __getstate__(self):
+ return {}
+
+
+# used for separating keyword arguments; we do not use an object
+# instance here so identity is preserved when pickling/unpickling
+_kwmark = (_HashedTuple,)
+
+
+def hashkey(*args, **kwargs):
+ """Return a cache key for the specified hashable arguments."""
+
+ if kwargs:
+ return _HashedTuple(args + sum(sorted(kwargs.items()), _kwmark))
+ else:
+ return _HashedTuple(args)
+
+
+def typedkey(*args, **kwargs):
+ """Return a typed cache key for the specified hashable arguments."""
+
+ key = hashkey(*args, **kwargs)
+ key += tuple(type(v) for v in args)
+ key += tuple(type(v) for _, v in sorted(kwargs.items()))
+ return key
diff --git a/telegramer/include/pytz/__init__.py b/telegramer/include/pytz/__init__.py
new file mode 100644
index 0000000..900e8ca
--- /dev/null
+++ b/telegramer/include/pytz/__init__.py
@@ -0,0 +1,1559 @@
+'''
+datetime.tzinfo timezone definitions generated from the
+Olson timezone database:
+
+ ftp://elsie.nci.nih.gov/pub/tz*.tar.gz
+
+See the datetime section of the Python Library Reference for information
+on how to use these modules.
+'''
+
+import sys
+import datetime
+import os.path
+
+from pytz.exceptions import AmbiguousTimeError
+from pytz.exceptions import InvalidTimeError
+from pytz.exceptions import NonExistentTimeError
+from pytz.exceptions import UnknownTimeZoneError
+from pytz.lazy import LazyDict, LazyList, LazySet # noqa
+from pytz.tzinfo import unpickler, BaseTzInfo
+from pytz.tzfile import build_tzinfo
+
+
+# The IANA (nee Olson) database is updated several times a year.
+OLSON_VERSION = '2022a'
+VERSION = '2022.1' # pip compatible version number.
+__version__ = VERSION
+
+OLSEN_VERSION = OLSON_VERSION # Old releases had this misspelling
+
+__all__ = [
+ 'timezone', 'utc', 'country_timezones', 'country_names',
+ 'AmbiguousTimeError', 'InvalidTimeError',
+ 'NonExistentTimeError', 'UnknownTimeZoneError',
+ 'all_timezones', 'all_timezones_set',
+ 'common_timezones', 'common_timezones_set',
+ 'BaseTzInfo', 'FixedOffset',
+]
+
+
+if sys.version_info[0] > 2: # Python 3.x
+
+ # Python 3.x doesn't have unicode(), making writing code
+ # for Python 2.3 and Python 3.x a pain.
+ unicode = str
+
+ def ascii(s):
+ r"""
+ >>> ascii('Hello')
+ 'Hello'
+ >>> ascii('\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ UnicodeEncodeError: ...
+ """
+ if type(s) == bytes:
+ s = s.decode('ASCII')
+ else:
+ s.encode('ASCII') # Raise an exception if not ASCII
+ return s # But the string - not a byte string.
+
+else: # Python 2.x
+
+ def ascii(s):
+ r"""
+ >>> ascii('Hello')
+ 'Hello'
+ >>> ascii(u'Hello')
+ 'Hello'
+ >>> ascii(u'\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ UnicodeEncodeError: ...
+ """
+ return s.encode('ASCII')
+
+
+def open_resource(name):
+ """Open a resource from the zoneinfo subdir for reading.
+
+ Uses the pkg_resources module if available and no standard file
+ found at the calculated location.
+
+ It is possible to specify different location for zoneinfo
+ subdir by using the PYTZ_TZDATADIR environment variable.
+ """
+ name_parts = name.lstrip('/').split('/')
+ for part in name_parts:
+ if part == os.path.pardir or os.path.sep in part:
+ raise ValueError('Bad path segment: %r' % part)
+ zoneinfo_dir = os.environ.get('PYTZ_TZDATADIR', None)
+ if zoneinfo_dir is not None:
+ filename = os.path.join(zoneinfo_dir, *name_parts)
+ else:
+ filename = os.path.join(os.path.dirname(__file__),
+ 'zoneinfo', *name_parts)
+ if not os.path.exists(filename):
+ # http://bugs.launchpad.net/bugs/383171 - we avoid using this
+ # unless absolutely necessary to help when a broken version of
+ # pkg_resources is installed.
+ try:
+ from pkg_resources import resource_stream
+ except ImportError:
+ resource_stream = None
+
+ if resource_stream is not None:
+ return resource_stream(__name__, 'zoneinfo/' + name)
+ return open(filename, 'rb')
+
+
+def resource_exists(name):
+ """Return true if the given resource exists"""
+ try:
+ if os.environ.get('PYTZ_SKIPEXISTSCHECK', ''):
+ # In "standard" distributions, we can assume that
+ # all the listed timezones are present. As an
+ # import-speed optimization, you can set the
+ # PYTZ_SKIPEXISTSCHECK flag to skip checking
+ # for the presence of the resource file on disk.
+ return True
+ open_resource(name).close()
+ return True
+ except IOError:
+ return False
+
+
+_tzinfo_cache = {}
+
+
+def timezone(zone):
+ r''' Return a datetime.tzinfo implementation for the given timezone
+
+ >>> from datetime import datetime, timedelta
+ >>> utc = timezone('UTC')
+ >>> eastern = timezone('US/Eastern')
+ >>> eastern.zone
+ 'US/Eastern'
+ >>> timezone(unicode('US/Eastern')) is eastern
+ True
+ >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)
+ >>> loc_dt = utc_dt.astimezone(eastern)
+ >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
+ >>> loc_dt.strftime(fmt)
+ '2002-10-27 01:00:00 EST (-0500)'
+ >>> (loc_dt - timedelta(minutes=10)).strftime(fmt)
+ '2002-10-27 00:50:00 EST (-0500)'
+ >>> eastern.normalize(loc_dt - timedelta(minutes=10)).strftime(fmt)
+ '2002-10-27 01:50:00 EDT (-0400)'
+ >>> (loc_dt + timedelta(minutes=10)).strftime(fmt)
+ '2002-10-27 01:10:00 EST (-0500)'
+
+ Raises UnknownTimeZoneError if passed an unknown zone.
+
+ >>> try:
+ ... timezone('Asia/Shangri-La')
+ ... except UnknownTimeZoneError:
+ ... print('Unknown')
+ Unknown
+
+ >>> try:
+ ... timezone(unicode('\N{TRADE MARK SIGN}'))
+ ... except UnknownTimeZoneError:
+ ... print('Unknown')
+ Unknown
+
+ '''
+ if zone is None:
+ raise UnknownTimeZoneError(None)
+
+ if zone.upper() == 'UTC':
+ return utc
+
+ try:
+ zone = ascii(zone)
+ except UnicodeEncodeError:
+ # All valid timezones are ASCII
+ raise UnknownTimeZoneError(zone)
+
+ zone = _case_insensitive_zone_lookup(_unmunge_zone(zone))
+ if zone not in _tzinfo_cache:
+ if zone in all_timezones_set: # noqa
+ fp = open_resource(zone)
+ try:
+ _tzinfo_cache[zone] = build_tzinfo(zone, fp)
+ finally:
+ fp.close()
+ else:
+ raise UnknownTimeZoneError(zone)
+
+ return _tzinfo_cache[zone]
+
+
+def _unmunge_zone(zone):
+ """Undo the time zone name munging done by older versions of pytz."""
+ return zone.replace('_plus_', '+').replace('_minus_', '-')
+
+
+_all_timezones_lower_to_standard = None
+
+
+def _case_insensitive_zone_lookup(zone):
+ """case-insensitively matching timezone, else return zone unchanged"""
+ global _all_timezones_lower_to_standard
+ if _all_timezones_lower_to_standard is None:
+ _all_timezones_lower_to_standard = dict((tz.lower(), tz) for tz in all_timezones) # noqa
+ return _all_timezones_lower_to_standard.get(zone.lower()) or zone # noqa
+
+
+ZERO = datetime.timedelta(0)
+HOUR = datetime.timedelta(hours=1)
+
+
+class UTC(BaseTzInfo):
+ """UTC
+
+ Optimized UTC implementation. It unpickles using the single module global
+ instance defined beneath this class declaration.
+ """
+ zone = "UTC"
+
+ _utcoffset = ZERO
+ _dst = ZERO
+ _tzname = zone
+
+ def fromutc(self, dt):
+ if dt.tzinfo is None:
+ return self.localize(dt)
+ return super(utc.__class__, self).fromutc(dt)
+
+ def utcoffset(self, dt):
+ return ZERO
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def dst(self, dt):
+ return ZERO
+
+ def __reduce__(self):
+ return _UTC, ()
+
+ def localize(self, dt, is_dst=False):
+ '''Convert naive time to local time'''
+ if dt.tzinfo is not None:
+ raise ValueError('Not naive datetime (tzinfo is already set)')
+ return dt.replace(tzinfo=self)
+
+ def normalize(self, dt, is_dst=False):
+ '''Correct the timezone information on the given datetime'''
+ if dt.tzinfo is self:
+ return dt
+ if dt.tzinfo is None:
+ raise ValueError('Naive time - no tzinfo set')
+ return dt.astimezone(self)
+
+ def __repr__(self):
+ return ""
+
+ def __str__(self):
+ return "UTC"
+
+
+UTC = utc = UTC() # UTC is a singleton
+
+
+def _UTC():
+ """Factory function for utc unpickling.
+
+ Makes sure that unpickling a utc instance always returns the same
+ module global.
+
+ These examples belong in the UTC class above, but it is obscured; or in
+ the README.rst, but we are not depending on Python 2.4 so integrating
+ the README.rst examples with the unit tests is not trivial.
+
+ >>> import datetime, pickle
+ >>> dt = datetime.datetime(2005, 3, 1, 14, 13, 21, tzinfo=utc)
+ >>> naive = dt.replace(tzinfo=None)
+ >>> p = pickle.dumps(dt, 1)
+ >>> naive_p = pickle.dumps(naive, 1)
+ >>> len(p) - len(naive_p)
+ 17
+ >>> new = pickle.loads(p)
+ >>> new == dt
+ True
+ >>> new is dt
+ False
+ >>> new.tzinfo is dt.tzinfo
+ True
+ >>> utc is UTC is timezone('UTC')
+ True
+ >>> utc is timezone('GMT')
+ False
+ """
+ return utc
+
+
+_UTC.__safe_for_unpickling__ = True
+
+
+def _p(*args):
+ """Factory function for unpickling pytz tzinfo instances.
+
+ Just a wrapper around tzinfo.unpickler to save a few bytes in each pickle
+ by shortening the path.
+ """
+ return unpickler(*args)
+
+
+_p.__safe_for_unpickling__ = True
+
+
+class _CountryTimezoneDict(LazyDict):
+ """Map ISO 3166 country code to a list of timezone names commonly used
+ in that country.
+
+ iso3166_code is the two letter code used to identify the country.
+
+ >>> def print_list(list_of_strings):
+ ... 'We use a helper so doctests work under Python 2.3 -> 3.x'
+ ... for s in list_of_strings:
+ ... print(s)
+
+ >>> print_list(country_timezones['nz'])
+ Pacific/Auckland
+ Pacific/Chatham
+ >>> print_list(country_timezones['ch'])
+ Europe/Zurich
+ >>> print_list(country_timezones['CH'])
+ Europe/Zurich
+ >>> print_list(country_timezones[unicode('ch')])
+ Europe/Zurich
+ >>> print_list(country_timezones['XXX'])
+ Traceback (most recent call last):
+ ...
+ KeyError: 'XXX'
+
+ Previously, this information was exposed as a function rather than a
+ dictionary. This is still supported::
+
+ >>> print_list(country_timezones('nz'))
+ Pacific/Auckland
+ Pacific/Chatham
+ """
+ def __call__(self, iso3166_code):
+ """Backwards compatibility."""
+ return self[iso3166_code]
+
+ def _fill(self):
+ data = {}
+ zone_tab = open_resource('zone.tab')
+ try:
+ for line in zone_tab:
+ line = line.decode('UTF-8')
+ if line.startswith('#'):
+ continue
+ code, coordinates, zone = line.split(None, 4)[:3]
+ if zone not in all_timezones_set: # noqa
+ continue
+ try:
+ data[code].append(zone)
+ except KeyError:
+ data[code] = [zone]
+ self.data = data
+ finally:
+ zone_tab.close()
+
+
+country_timezones = _CountryTimezoneDict()
+
+
+class _CountryNameDict(LazyDict):
+ '''Dictionary proving ISO3166 code -> English name.
+
+ >>> print(country_names['au'])
+ Australia
+ '''
+ def _fill(self):
+ data = {}
+ zone_tab = open_resource('iso3166.tab')
+ try:
+ for line in zone_tab.readlines():
+ line = line.decode('UTF-8')
+ if line.startswith('#'):
+ continue
+ code, name = line.split(None, 1)
+ data[code] = name.strip()
+ self.data = data
+ finally:
+ zone_tab.close()
+
+
+country_names = _CountryNameDict()
+
+
+# Time-zone info based solely on fixed offsets
+
+class _FixedOffset(datetime.tzinfo):
+
+ zone = None # to match the standard pytz API
+
+ def __init__(self, minutes):
+ if abs(minutes) >= 1440:
+ raise ValueError("absolute offset is too large", minutes)
+ self._minutes = minutes
+ self._offset = datetime.timedelta(minutes=minutes)
+
+ def utcoffset(self, dt):
+ return self._offset
+
+ def __reduce__(self):
+ return FixedOffset, (self._minutes, )
+
+ def dst(self, dt):
+ return ZERO
+
+ def tzname(self, dt):
+ return None
+
+ def __repr__(self):
+ return 'pytz.FixedOffset(%d)' % self._minutes
+
+ def localize(self, dt, is_dst=False):
+ '''Convert naive time to local time'''
+ if dt.tzinfo is not None:
+ raise ValueError('Not naive datetime (tzinfo is already set)')
+ return dt.replace(tzinfo=self)
+
+ def normalize(self, dt, is_dst=False):
+ '''Correct the timezone information on the given datetime'''
+ if dt.tzinfo is self:
+ return dt
+ if dt.tzinfo is None:
+ raise ValueError('Naive time - no tzinfo set')
+ return dt.astimezone(self)
+
+
+def FixedOffset(offset, _tzinfos={}):
+ """return a fixed-offset timezone based off a number of minutes.
+
+ >>> one = FixedOffset(-330)
+ >>> one
+ pytz.FixedOffset(-330)
+ >>> str(one.utcoffset(datetime.datetime.now()))
+ '-1 day, 18:30:00'
+ >>> str(one.dst(datetime.datetime.now()))
+ '0:00:00'
+
+ >>> two = FixedOffset(1380)
+ >>> two
+ pytz.FixedOffset(1380)
+ >>> str(two.utcoffset(datetime.datetime.now()))
+ '23:00:00'
+ >>> str(two.dst(datetime.datetime.now()))
+ '0:00:00'
+
+ The datetime.timedelta must be between the range of -1 and 1 day,
+ non-inclusive.
+
+ >>> FixedOffset(1440)
+ Traceback (most recent call last):
+ ...
+ ValueError: ('absolute offset is too large', 1440)
+
+ >>> FixedOffset(-1440)
+ Traceback (most recent call last):
+ ...
+ ValueError: ('absolute offset is too large', -1440)
+
+ An offset of 0 is special-cased to return UTC.
+
+ >>> FixedOffset(0) is UTC
+ True
+
+ There should always be only one instance of a FixedOffset per timedelta.
+ This should be true for multiple creation calls.
+
+ >>> FixedOffset(-330) is one
+ True
+ >>> FixedOffset(1380) is two
+ True
+
+ It should also be true for pickling.
+
+ >>> import pickle
+ >>> pickle.loads(pickle.dumps(one)) is one
+ True
+ >>> pickle.loads(pickle.dumps(two)) is two
+ True
+ """
+ if offset == 0:
+ return UTC
+
+ info = _tzinfos.get(offset)
+ if info is None:
+ # We haven't seen this one before. we need to save it.
+
+ # Use setdefault to avoid a race condition and make sure we have
+ # only one
+ info = _tzinfos.setdefault(offset, _FixedOffset(offset))
+
+ return info
+
+
+FixedOffset.__safe_for_unpickling__ = True
+
+
+def _test():
+ import doctest
+ sys.path.insert(0, os.pardir)
+ import pytz
+ return doctest.testmod(pytz)
+
+
+if __name__ == '__main__':
+ _test()
+all_timezones = \
+['Africa/Abidjan',
+ 'Africa/Accra',
+ 'Africa/Addis_Ababa',
+ 'Africa/Algiers',
+ 'Africa/Asmara',
+ 'Africa/Asmera',
+ 'Africa/Bamako',
+ 'Africa/Bangui',
+ 'Africa/Banjul',
+ 'Africa/Bissau',
+ 'Africa/Blantyre',
+ 'Africa/Brazzaville',
+ 'Africa/Bujumbura',
+ 'Africa/Cairo',
+ 'Africa/Casablanca',
+ 'Africa/Ceuta',
+ 'Africa/Conakry',
+ 'Africa/Dakar',
+ 'Africa/Dar_es_Salaam',
+ 'Africa/Djibouti',
+ 'Africa/Douala',
+ 'Africa/El_Aaiun',
+ 'Africa/Freetown',
+ 'Africa/Gaborone',
+ 'Africa/Harare',
+ 'Africa/Johannesburg',
+ 'Africa/Juba',
+ 'Africa/Kampala',
+ 'Africa/Khartoum',
+ 'Africa/Kigali',
+ 'Africa/Kinshasa',
+ 'Africa/Lagos',
+ 'Africa/Libreville',
+ 'Africa/Lome',
+ 'Africa/Luanda',
+ 'Africa/Lubumbashi',
+ 'Africa/Lusaka',
+ 'Africa/Malabo',
+ 'Africa/Maputo',
+ 'Africa/Maseru',
+ 'Africa/Mbabane',
+ 'Africa/Mogadishu',
+ 'Africa/Monrovia',
+ 'Africa/Nairobi',
+ 'Africa/Ndjamena',
+ 'Africa/Niamey',
+ 'Africa/Nouakchott',
+ 'Africa/Ouagadougou',
+ 'Africa/Porto-Novo',
+ 'Africa/Sao_Tome',
+ 'Africa/Timbuktu',
+ 'Africa/Tripoli',
+ 'Africa/Tunis',
+ 'Africa/Windhoek',
+ 'America/Adak',
+ 'America/Anchorage',
+ 'America/Anguilla',
+ 'America/Antigua',
+ 'America/Araguaina',
+ 'America/Argentina/Buenos_Aires',
+ 'America/Argentina/Catamarca',
+ 'America/Argentina/ComodRivadavia',
+ 'America/Argentina/Cordoba',
+ 'America/Argentina/Jujuy',
+ 'America/Argentina/La_Rioja',
+ 'America/Argentina/Mendoza',
+ 'America/Argentina/Rio_Gallegos',
+ 'America/Argentina/Salta',
+ 'America/Argentina/San_Juan',
+ 'America/Argentina/San_Luis',
+ 'America/Argentina/Tucuman',
+ 'America/Argentina/Ushuaia',
+ 'America/Aruba',
+ 'America/Asuncion',
+ 'America/Atikokan',
+ 'America/Atka',
+ 'America/Bahia',
+ 'America/Bahia_Banderas',
+ 'America/Barbados',
+ 'America/Belem',
+ 'America/Belize',
+ 'America/Blanc-Sablon',
+ 'America/Boa_Vista',
+ 'America/Bogota',
+ 'America/Boise',
+ 'America/Buenos_Aires',
+ 'America/Cambridge_Bay',
+ 'America/Campo_Grande',
+ 'America/Cancun',
+ 'America/Caracas',
+ 'America/Catamarca',
+ 'America/Cayenne',
+ 'America/Cayman',
+ 'America/Chicago',
+ 'America/Chihuahua',
+ 'America/Coral_Harbour',
+ 'America/Cordoba',
+ 'America/Costa_Rica',
+ 'America/Creston',
+ 'America/Cuiaba',
+ 'America/Curacao',
+ 'America/Danmarkshavn',
+ 'America/Dawson',
+ 'America/Dawson_Creek',
+ 'America/Denver',
+ 'America/Detroit',
+ 'America/Dominica',
+ 'America/Edmonton',
+ 'America/Eirunepe',
+ 'America/El_Salvador',
+ 'America/Ensenada',
+ 'America/Fort_Nelson',
+ 'America/Fort_Wayne',
+ 'America/Fortaleza',
+ 'America/Glace_Bay',
+ 'America/Godthab',
+ 'America/Goose_Bay',
+ 'America/Grand_Turk',
+ 'America/Grenada',
+ 'America/Guadeloupe',
+ 'America/Guatemala',
+ 'America/Guayaquil',
+ 'America/Guyana',
+ 'America/Halifax',
+ 'America/Havana',
+ 'America/Hermosillo',
+ 'America/Indiana/Indianapolis',
+ 'America/Indiana/Knox',
+ 'America/Indiana/Marengo',
+ 'America/Indiana/Petersburg',
+ 'America/Indiana/Tell_City',
+ 'America/Indiana/Vevay',
+ 'America/Indiana/Vincennes',
+ 'America/Indiana/Winamac',
+ 'America/Indianapolis',
+ 'America/Inuvik',
+ 'America/Iqaluit',
+ 'America/Jamaica',
+ 'America/Jujuy',
+ 'America/Juneau',
+ 'America/Kentucky/Louisville',
+ 'America/Kentucky/Monticello',
+ 'America/Knox_IN',
+ 'America/Kralendijk',
+ 'America/La_Paz',
+ 'America/Lima',
+ 'America/Los_Angeles',
+ 'America/Louisville',
+ 'America/Lower_Princes',
+ 'America/Maceio',
+ 'America/Managua',
+ 'America/Manaus',
+ 'America/Marigot',
+ 'America/Martinique',
+ 'America/Matamoros',
+ 'America/Mazatlan',
+ 'America/Mendoza',
+ 'America/Menominee',
+ 'America/Merida',
+ 'America/Metlakatla',
+ 'America/Mexico_City',
+ 'America/Miquelon',
+ 'America/Moncton',
+ 'America/Monterrey',
+ 'America/Montevideo',
+ 'America/Montreal',
+ 'America/Montserrat',
+ 'America/Nassau',
+ 'America/New_York',
+ 'America/Nipigon',
+ 'America/Nome',
+ 'America/Noronha',
+ 'America/North_Dakota/Beulah',
+ 'America/North_Dakota/Center',
+ 'America/North_Dakota/New_Salem',
+ 'America/Nuuk',
+ 'America/Ojinaga',
+ 'America/Panama',
+ 'America/Pangnirtung',
+ 'America/Paramaribo',
+ 'America/Phoenix',
+ 'America/Port-au-Prince',
+ 'America/Port_of_Spain',
+ 'America/Porto_Acre',
+ 'America/Porto_Velho',
+ 'America/Puerto_Rico',
+ 'America/Punta_Arenas',
+ 'America/Rainy_River',
+ 'America/Rankin_Inlet',
+ 'America/Recife',
+ 'America/Regina',
+ 'America/Resolute',
+ 'America/Rio_Branco',
+ 'America/Rosario',
+ 'America/Santa_Isabel',
+ 'America/Santarem',
+ 'America/Santiago',
+ 'America/Santo_Domingo',
+ 'America/Sao_Paulo',
+ 'America/Scoresbysund',
+ 'America/Shiprock',
+ 'America/Sitka',
+ 'America/St_Barthelemy',
+ 'America/St_Johns',
+ 'America/St_Kitts',
+ 'America/St_Lucia',
+ 'America/St_Thomas',
+ 'America/St_Vincent',
+ 'America/Swift_Current',
+ 'America/Tegucigalpa',
+ 'America/Thule',
+ 'America/Thunder_Bay',
+ 'America/Tijuana',
+ 'America/Toronto',
+ 'America/Tortola',
+ 'America/Vancouver',
+ 'America/Virgin',
+ 'America/Whitehorse',
+ 'America/Winnipeg',
+ 'America/Yakutat',
+ 'America/Yellowknife',
+ 'Antarctica/Casey',
+ 'Antarctica/Davis',
+ 'Antarctica/DumontDUrville',
+ 'Antarctica/Macquarie',
+ 'Antarctica/Mawson',
+ 'Antarctica/McMurdo',
+ 'Antarctica/Palmer',
+ 'Antarctica/Rothera',
+ 'Antarctica/South_Pole',
+ 'Antarctica/Syowa',
+ 'Antarctica/Troll',
+ 'Antarctica/Vostok',
+ 'Arctic/Longyearbyen',
+ 'Asia/Aden',
+ 'Asia/Almaty',
+ 'Asia/Amman',
+ 'Asia/Anadyr',
+ 'Asia/Aqtau',
+ 'Asia/Aqtobe',
+ 'Asia/Ashgabat',
+ 'Asia/Ashkhabad',
+ 'Asia/Atyrau',
+ 'Asia/Baghdad',
+ 'Asia/Bahrain',
+ 'Asia/Baku',
+ 'Asia/Bangkok',
+ 'Asia/Barnaul',
+ 'Asia/Beirut',
+ 'Asia/Bishkek',
+ 'Asia/Brunei',
+ 'Asia/Calcutta',
+ 'Asia/Chita',
+ 'Asia/Choibalsan',
+ 'Asia/Chongqing',
+ 'Asia/Chungking',
+ 'Asia/Colombo',
+ 'Asia/Dacca',
+ 'Asia/Damascus',
+ 'Asia/Dhaka',
+ 'Asia/Dili',
+ 'Asia/Dubai',
+ 'Asia/Dushanbe',
+ 'Asia/Famagusta',
+ 'Asia/Gaza',
+ 'Asia/Harbin',
+ 'Asia/Hebron',
+ 'Asia/Ho_Chi_Minh',
+ 'Asia/Hong_Kong',
+ 'Asia/Hovd',
+ 'Asia/Irkutsk',
+ 'Asia/Istanbul',
+ 'Asia/Jakarta',
+ 'Asia/Jayapura',
+ 'Asia/Jerusalem',
+ 'Asia/Kabul',
+ 'Asia/Kamchatka',
+ 'Asia/Karachi',
+ 'Asia/Kashgar',
+ 'Asia/Kathmandu',
+ 'Asia/Katmandu',
+ 'Asia/Khandyga',
+ 'Asia/Kolkata',
+ 'Asia/Krasnoyarsk',
+ 'Asia/Kuala_Lumpur',
+ 'Asia/Kuching',
+ 'Asia/Kuwait',
+ 'Asia/Macao',
+ 'Asia/Macau',
+ 'Asia/Magadan',
+ 'Asia/Makassar',
+ 'Asia/Manila',
+ 'Asia/Muscat',
+ 'Asia/Nicosia',
+ 'Asia/Novokuznetsk',
+ 'Asia/Novosibirsk',
+ 'Asia/Omsk',
+ 'Asia/Oral',
+ 'Asia/Phnom_Penh',
+ 'Asia/Pontianak',
+ 'Asia/Pyongyang',
+ 'Asia/Qatar',
+ 'Asia/Qostanay',
+ 'Asia/Qyzylorda',
+ 'Asia/Rangoon',
+ 'Asia/Riyadh',
+ 'Asia/Saigon',
+ 'Asia/Sakhalin',
+ 'Asia/Samarkand',
+ 'Asia/Seoul',
+ 'Asia/Shanghai',
+ 'Asia/Singapore',
+ 'Asia/Srednekolymsk',
+ 'Asia/Taipei',
+ 'Asia/Tashkent',
+ 'Asia/Tbilisi',
+ 'Asia/Tehran',
+ 'Asia/Tel_Aviv',
+ 'Asia/Thimbu',
+ 'Asia/Thimphu',
+ 'Asia/Tokyo',
+ 'Asia/Tomsk',
+ 'Asia/Ujung_Pandang',
+ 'Asia/Ulaanbaatar',
+ 'Asia/Ulan_Bator',
+ 'Asia/Urumqi',
+ 'Asia/Ust-Nera',
+ 'Asia/Vientiane',
+ 'Asia/Vladivostok',
+ 'Asia/Yakutsk',
+ 'Asia/Yangon',
+ 'Asia/Yekaterinburg',
+ 'Asia/Yerevan',
+ 'Atlantic/Azores',
+ 'Atlantic/Bermuda',
+ 'Atlantic/Canary',
+ 'Atlantic/Cape_Verde',
+ 'Atlantic/Faeroe',
+ 'Atlantic/Faroe',
+ 'Atlantic/Jan_Mayen',
+ 'Atlantic/Madeira',
+ 'Atlantic/Reykjavik',
+ 'Atlantic/South_Georgia',
+ 'Atlantic/St_Helena',
+ 'Atlantic/Stanley',
+ 'Australia/ACT',
+ 'Australia/Adelaide',
+ 'Australia/Brisbane',
+ 'Australia/Broken_Hill',
+ 'Australia/Canberra',
+ 'Australia/Currie',
+ 'Australia/Darwin',
+ 'Australia/Eucla',
+ 'Australia/Hobart',
+ 'Australia/LHI',
+ 'Australia/Lindeman',
+ 'Australia/Lord_Howe',
+ 'Australia/Melbourne',
+ 'Australia/NSW',
+ 'Australia/North',
+ 'Australia/Perth',
+ 'Australia/Queensland',
+ 'Australia/South',
+ 'Australia/Sydney',
+ 'Australia/Tasmania',
+ 'Australia/Victoria',
+ 'Australia/West',
+ 'Australia/Yancowinna',
+ 'Brazil/Acre',
+ 'Brazil/DeNoronha',
+ 'Brazil/East',
+ 'Brazil/West',
+ 'CET',
+ 'CST6CDT',
+ 'Canada/Atlantic',
+ 'Canada/Central',
+ 'Canada/Eastern',
+ 'Canada/Mountain',
+ 'Canada/Newfoundland',
+ 'Canada/Pacific',
+ 'Canada/Saskatchewan',
+ 'Canada/Yukon',
+ 'Chile/Continental',
+ 'Chile/EasterIsland',
+ 'Cuba',
+ 'EET',
+ 'EST',
+ 'EST5EDT',
+ 'Egypt',
+ 'Eire',
+ 'Etc/GMT',
+ 'Etc/GMT+0',
+ 'Etc/GMT+1',
+ 'Etc/GMT+10',
+ 'Etc/GMT+11',
+ 'Etc/GMT+12',
+ 'Etc/GMT+2',
+ 'Etc/GMT+3',
+ 'Etc/GMT+4',
+ 'Etc/GMT+5',
+ 'Etc/GMT+6',
+ 'Etc/GMT+7',
+ 'Etc/GMT+8',
+ 'Etc/GMT+9',
+ 'Etc/GMT-0',
+ 'Etc/GMT-1',
+ 'Etc/GMT-10',
+ 'Etc/GMT-11',
+ 'Etc/GMT-12',
+ 'Etc/GMT-13',
+ 'Etc/GMT-14',
+ 'Etc/GMT-2',
+ 'Etc/GMT-3',
+ 'Etc/GMT-4',
+ 'Etc/GMT-5',
+ 'Etc/GMT-6',
+ 'Etc/GMT-7',
+ 'Etc/GMT-8',
+ 'Etc/GMT-9',
+ 'Etc/GMT0',
+ 'Etc/Greenwich',
+ 'Etc/UCT',
+ 'Etc/UTC',
+ 'Etc/Universal',
+ 'Etc/Zulu',
+ 'Europe/Amsterdam',
+ 'Europe/Andorra',
+ 'Europe/Astrakhan',
+ 'Europe/Athens',
+ 'Europe/Belfast',
+ 'Europe/Belgrade',
+ 'Europe/Berlin',
+ 'Europe/Bratislava',
+ 'Europe/Brussels',
+ 'Europe/Bucharest',
+ 'Europe/Budapest',
+ 'Europe/Busingen',
+ 'Europe/Chisinau',
+ 'Europe/Copenhagen',
+ 'Europe/Dublin',
+ 'Europe/Gibraltar',
+ 'Europe/Guernsey',
+ 'Europe/Helsinki',
+ 'Europe/Isle_of_Man',
+ 'Europe/Istanbul',
+ 'Europe/Jersey',
+ 'Europe/Kaliningrad',
+ 'Europe/Kiev',
+ 'Europe/Kirov',
+ 'Europe/Lisbon',
+ 'Europe/Ljubljana',
+ 'Europe/London',
+ 'Europe/Luxembourg',
+ 'Europe/Madrid',
+ 'Europe/Malta',
+ 'Europe/Mariehamn',
+ 'Europe/Minsk',
+ 'Europe/Monaco',
+ 'Europe/Moscow',
+ 'Europe/Nicosia',
+ 'Europe/Oslo',
+ 'Europe/Paris',
+ 'Europe/Podgorica',
+ 'Europe/Prague',
+ 'Europe/Riga',
+ 'Europe/Rome',
+ 'Europe/Samara',
+ 'Europe/San_Marino',
+ 'Europe/Sarajevo',
+ 'Europe/Saratov',
+ 'Europe/Simferopol',
+ 'Europe/Skopje',
+ 'Europe/Sofia',
+ 'Europe/Stockholm',
+ 'Europe/Tallinn',
+ 'Europe/Tirane',
+ 'Europe/Tiraspol',
+ 'Europe/Ulyanovsk',
+ 'Europe/Uzhgorod',
+ 'Europe/Vaduz',
+ 'Europe/Vatican',
+ 'Europe/Vienna',
+ 'Europe/Vilnius',
+ 'Europe/Volgograd',
+ 'Europe/Warsaw',
+ 'Europe/Zagreb',
+ 'Europe/Zaporozhye',
+ 'Europe/Zurich',
+ 'GB',
+ 'GB-Eire',
+ 'GMT',
+ 'GMT+0',
+ 'GMT-0',
+ 'GMT0',
+ 'Greenwich',
+ 'HST',
+ 'Hongkong',
+ 'Iceland',
+ 'Indian/Antananarivo',
+ 'Indian/Chagos',
+ 'Indian/Christmas',
+ 'Indian/Cocos',
+ 'Indian/Comoro',
+ 'Indian/Kerguelen',
+ 'Indian/Mahe',
+ 'Indian/Maldives',
+ 'Indian/Mauritius',
+ 'Indian/Mayotte',
+ 'Indian/Reunion',
+ 'Iran',
+ 'Israel',
+ 'Jamaica',
+ 'Japan',
+ 'Kwajalein',
+ 'Libya',
+ 'MET',
+ 'MST',
+ 'MST7MDT',
+ 'Mexico/BajaNorte',
+ 'Mexico/BajaSur',
+ 'Mexico/General',
+ 'NZ',
+ 'NZ-CHAT',
+ 'Navajo',
+ 'PRC',
+ 'PST8PDT',
+ 'Pacific/Apia',
+ 'Pacific/Auckland',
+ 'Pacific/Bougainville',
+ 'Pacific/Chatham',
+ 'Pacific/Chuuk',
+ 'Pacific/Easter',
+ 'Pacific/Efate',
+ 'Pacific/Enderbury',
+ 'Pacific/Fakaofo',
+ 'Pacific/Fiji',
+ 'Pacific/Funafuti',
+ 'Pacific/Galapagos',
+ 'Pacific/Gambier',
+ 'Pacific/Guadalcanal',
+ 'Pacific/Guam',
+ 'Pacific/Honolulu',
+ 'Pacific/Johnston',
+ 'Pacific/Kanton',
+ 'Pacific/Kiritimati',
+ 'Pacific/Kosrae',
+ 'Pacific/Kwajalein',
+ 'Pacific/Majuro',
+ 'Pacific/Marquesas',
+ 'Pacific/Midway',
+ 'Pacific/Nauru',
+ 'Pacific/Niue',
+ 'Pacific/Norfolk',
+ 'Pacific/Noumea',
+ 'Pacific/Pago_Pago',
+ 'Pacific/Palau',
+ 'Pacific/Pitcairn',
+ 'Pacific/Pohnpei',
+ 'Pacific/Ponape',
+ 'Pacific/Port_Moresby',
+ 'Pacific/Rarotonga',
+ 'Pacific/Saipan',
+ 'Pacific/Samoa',
+ 'Pacific/Tahiti',
+ 'Pacific/Tarawa',
+ 'Pacific/Tongatapu',
+ 'Pacific/Truk',
+ 'Pacific/Wake',
+ 'Pacific/Wallis',
+ 'Pacific/Yap',
+ 'Poland',
+ 'Portugal',
+ 'ROC',
+ 'ROK',
+ 'Singapore',
+ 'Turkey',
+ 'UCT',
+ 'US/Alaska',
+ 'US/Aleutian',
+ 'US/Arizona',
+ 'US/Central',
+ 'US/East-Indiana',
+ 'US/Eastern',
+ 'US/Hawaii',
+ 'US/Indiana-Starke',
+ 'US/Michigan',
+ 'US/Mountain',
+ 'US/Pacific',
+ 'US/Samoa',
+ 'UTC',
+ 'Universal',
+ 'W-SU',
+ 'WET',
+ 'Zulu']
+all_timezones = LazyList(
+ tz for tz in all_timezones if resource_exists(tz))
+
+all_timezones_set = LazySet(all_timezones)
+common_timezones = \
+['Africa/Abidjan',
+ 'Africa/Accra',
+ 'Africa/Addis_Ababa',
+ 'Africa/Algiers',
+ 'Africa/Asmara',
+ 'Africa/Bamako',
+ 'Africa/Bangui',
+ 'Africa/Banjul',
+ 'Africa/Bissau',
+ 'Africa/Blantyre',
+ 'Africa/Brazzaville',
+ 'Africa/Bujumbura',
+ 'Africa/Cairo',
+ 'Africa/Casablanca',
+ 'Africa/Ceuta',
+ 'Africa/Conakry',
+ 'Africa/Dakar',
+ 'Africa/Dar_es_Salaam',
+ 'Africa/Djibouti',
+ 'Africa/Douala',
+ 'Africa/El_Aaiun',
+ 'Africa/Freetown',
+ 'Africa/Gaborone',
+ 'Africa/Harare',
+ 'Africa/Johannesburg',
+ 'Africa/Juba',
+ 'Africa/Kampala',
+ 'Africa/Khartoum',
+ 'Africa/Kigali',
+ 'Africa/Kinshasa',
+ 'Africa/Lagos',
+ 'Africa/Libreville',
+ 'Africa/Lome',
+ 'Africa/Luanda',
+ 'Africa/Lubumbashi',
+ 'Africa/Lusaka',
+ 'Africa/Malabo',
+ 'Africa/Maputo',
+ 'Africa/Maseru',
+ 'Africa/Mbabane',
+ 'Africa/Mogadishu',
+ 'Africa/Monrovia',
+ 'Africa/Nairobi',
+ 'Africa/Ndjamena',
+ 'Africa/Niamey',
+ 'Africa/Nouakchott',
+ 'Africa/Ouagadougou',
+ 'Africa/Porto-Novo',
+ 'Africa/Sao_Tome',
+ 'Africa/Tripoli',
+ 'Africa/Tunis',
+ 'Africa/Windhoek',
+ 'America/Adak',
+ 'America/Anchorage',
+ 'America/Anguilla',
+ 'America/Antigua',
+ 'America/Araguaina',
+ 'America/Argentina/Buenos_Aires',
+ 'America/Argentina/Catamarca',
+ 'America/Argentina/Cordoba',
+ 'America/Argentina/Jujuy',
+ 'America/Argentina/La_Rioja',
+ 'America/Argentina/Mendoza',
+ 'America/Argentina/Rio_Gallegos',
+ 'America/Argentina/Salta',
+ 'America/Argentina/San_Juan',
+ 'America/Argentina/San_Luis',
+ 'America/Argentina/Tucuman',
+ 'America/Argentina/Ushuaia',
+ 'America/Aruba',
+ 'America/Asuncion',
+ 'America/Atikokan',
+ 'America/Bahia',
+ 'America/Bahia_Banderas',
+ 'America/Barbados',
+ 'America/Belem',
+ 'America/Belize',
+ 'America/Blanc-Sablon',
+ 'America/Boa_Vista',
+ 'America/Bogota',
+ 'America/Boise',
+ 'America/Cambridge_Bay',
+ 'America/Campo_Grande',
+ 'America/Cancun',
+ 'America/Caracas',
+ 'America/Cayenne',
+ 'America/Cayman',
+ 'America/Chicago',
+ 'America/Chihuahua',
+ 'America/Costa_Rica',
+ 'America/Creston',
+ 'America/Cuiaba',
+ 'America/Curacao',
+ 'America/Danmarkshavn',
+ 'America/Dawson',
+ 'America/Dawson_Creek',
+ 'America/Denver',
+ 'America/Detroit',
+ 'America/Dominica',
+ 'America/Edmonton',
+ 'America/Eirunepe',
+ 'America/El_Salvador',
+ 'America/Fort_Nelson',
+ 'America/Fortaleza',
+ 'America/Glace_Bay',
+ 'America/Goose_Bay',
+ 'America/Grand_Turk',
+ 'America/Grenada',
+ 'America/Guadeloupe',
+ 'America/Guatemala',
+ 'America/Guayaquil',
+ 'America/Guyana',
+ 'America/Halifax',
+ 'America/Havana',
+ 'America/Hermosillo',
+ 'America/Indiana/Indianapolis',
+ 'America/Indiana/Knox',
+ 'America/Indiana/Marengo',
+ 'America/Indiana/Petersburg',
+ 'America/Indiana/Tell_City',
+ 'America/Indiana/Vevay',
+ 'America/Indiana/Vincennes',
+ 'America/Indiana/Winamac',
+ 'America/Inuvik',
+ 'America/Iqaluit',
+ 'America/Jamaica',
+ 'America/Juneau',
+ 'America/Kentucky/Louisville',
+ 'America/Kentucky/Monticello',
+ 'America/Kralendijk',
+ 'America/La_Paz',
+ 'America/Lima',
+ 'America/Los_Angeles',
+ 'America/Lower_Princes',
+ 'America/Maceio',
+ 'America/Managua',
+ 'America/Manaus',
+ 'America/Marigot',
+ 'America/Martinique',
+ 'America/Matamoros',
+ 'America/Mazatlan',
+ 'America/Menominee',
+ 'America/Merida',
+ 'America/Metlakatla',
+ 'America/Mexico_City',
+ 'America/Miquelon',
+ 'America/Moncton',
+ 'America/Monterrey',
+ 'America/Montevideo',
+ 'America/Montserrat',
+ 'America/Nassau',
+ 'America/New_York',
+ 'America/Nipigon',
+ 'America/Nome',
+ 'America/Noronha',
+ 'America/North_Dakota/Beulah',
+ 'America/North_Dakota/Center',
+ 'America/North_Dakota/New_Salem',
+ 'America/Nuuk',
+ 'America/Ojinaga',
+ 'America/Panama',
+ 'America/Pangnirtung',
+ 'America/Paramaribo',
+ 'America/Phoenix',
+ 'America/Port-au-Prince',
+ 'America/Port_of_Spain',
+ 'America/Porto_Velho',
+ 'America/Puerto_Rico',
+ 'America/Punta_Arenas',
+ 'America/Rainy_River',
+ 'America/Rankin_Inlet',
+ 'America/Recife',
+ 'America/Regina',
+ 'America/Resolute',
+ 'America/Rio_Branco',
+ 'America/Santarem',
+ 'America/Santiago',
+ 'America/Santo_Domingo',
+ 'America/Sao_Paulo',
+ 'America/Scoresbysund',
+ 'America/Sitka',
+ 'America/St_Barthelemy',
+ 'America/St_Johns',
+ 'America/St_Kitts',
+ 'America/St_Lucia',
+ 'America/St_Thomas',
+ 'America/St_Vincent',
+ 'America/Swift_Current',
+ 'America/Tegucigalpa',
+ 'America/Thule',
+ 'America/Thunder_Bay',
+ 'America/Tijuana',
+ 'America/Toronto',
+ 'America/Tortola',
+ 'America/Vancouver',
+ 'America/Whitehorse',
+ 'America/Winnipeg',
+ 'America/Yakutat',
+ 'America/Yellowknife',
+ 'Antarctica/Casey',
+ 'Antarctica/Davis',
+ 'Antarctica/DumontDUrville',
+ 'Antarctica/Macquarie',
+ 'Antarctica/Mawson',
+ 'Antarctica/McMurdo',
+ 'Antarctica/Palmer',
+ 'Antarctica/Rothera',
+ 'Antarctica/Syowa',
+ 'Antarctica/Troll',
+ 'Antarctica/Vostok',
+ 'Arctic/Longyearbyen',
+ 'Asia/Aden',
+ 'Asia/Almaty',
+ 'Asia/Amman',
+ 'Asia/Anadyr',
+ 'Asia/Aqtau',
+ 'Asia/Aqtobe',
+ 'Asia/Ashgabat',
+ 'Asia/Atyrau',
+ 'Asia/Baghdad',
+ 'Asia/Bahrain',
+ 'Asia/Baku',
+ 'Asia/Bangkok',
+ 'Asia/Barnaul',
+ 'Asia/Beirut',
+ 'Asia/Bishkek',
+ 'Asia/Brunei',
+ 'Asia/Chita',
+ 'Asia/Choibalsan',
+ 'Asia/Colombo',
+ 'Asia/Damascus',
+ 'Asia/Dhaka',
+ 'Asia/Dili',
+ 'Asia/Dubai',
+ 'Asia/Dushanbe',
+ 'Asia/Famagusta',
+ 'Asia/Gaza',
+ 'Asia/Hebron',
+ 'Asia/Ho_Chi_Minh',
+ 'Asia/Hong_Kong',
+ 'Asia/Hovd',
+ 'Asia/Irkutsk',
+ 'Asia/Jakarta',
+ 'Asia/Jayapura',
+ 'Asia/Jerusalem',
+ 'Asia/Kabul',
+ 'Asia/Kamchatka',
+ 'Asia/Karachi',
+ 'Asia/Kathmandu',
+ 'Asia/Khandyga',
+ 'Asia/Kolkata',
+ 'Asia/Krasnoyarsk',
+ 'Asia/Kuala_Lumpur',
+ 'Asia/Kuching',
+ 'Asia/Kuwait',
+ 'Asia/Macau',
+ 'Asia/Magadan',
+ 'Asia/Makassar',
+ 'Asia/Manila',
+ 'Asia/Muscat',
+ 'Asia/Nicosia',
+ 'Asia/Novokuznetsk',
+ 'Asia/Novosibirsk',
+ 'Asia/Omsk',
+ 'Asia/Oral',
+ 'Asia/Phnom_Penh',
+ 'Asia/Pontianak',
+ 'Asia/Pyongyang',
+ 'Asia/Qatar',
+ 'Asia/Qostanay',
+ 'Asia/Qyzylorda',
+ 'Asia/Riyadh',
+ 'Asia/Sakhalin',
+ 'Asia/Samarkand',
+ 'Asia/Seoul',
+ 'Asia/Shanghai',
+ 'Asia/Singapore',
+ 'Asia/Srednekolymsk',
+ 'Asia/Taipei',
+ 'Asia/Tashkent',
+ 'Asia/Tbilisi',
+ 'Asia/Tehran',
+ 'Asia/Thimphu',
+ 'Asia/Tokyo',
+ 'Asia/Tomsk',
+ 'Asia/Ulaanbaatar',
+ 'Asia/Urumqi',
+ 'Asia/Ust-Nera',
+ 'Asia/Vientiane',
+ 'Asia/Vladivostok',
+ 'Asia/Yakutsk',
+ 'Asia/Yangon',
+ 'Asia/Yekaterinburg',
+ 'Asia/Yerevan',
+ 'Atlantic/Azores',
+ 'Atlantic/Bermuda',
+ 'Atlantic/Canary',
+ 'Atlantic/Cape_Verde',
+ 'Atlantic/Faroe',
+ 'Atlantic/Madeira',
+ 'Atlantic/Reykjavik',
+ 'Atlantic/South_Georgia',
+ 'Atlantic/St_Helena',
+ 'Atlantic/Stanley',
+ 'Australia/Adelaide',
+ 'Australia/Brisbane',
+ 'Australia/Broken_Hill',
+ 'Australia/Darwin',
+ 'Australia/Eucla',
+ 'Australia/Hobart',
+ 'Australia/Lindeman',
+ 'Australia/Lord_Howe',
+ 'Australia/Melbourne',
+ 'Australia/Perth',
+ 'Australia/Sydney',
+ 'Canada/Atlantic',
+ 'Canada/Central',
+ 'Canada/Eastern',
+ 'Canada/Mountain',
+ 'Canada/Newfoundland',
+ 'Canada/Pacific',
+ 'Europe/Amsterdam',
+ 'Europe/Andorra',
+ 'Europe/Astrakhan',
+ 'Europe/Athens',
+ 'Europe/Belgrade',
+ 'Europe/Berlin',
+ 'Europe/Bratislava',
+ 'Europe/Brussels',
+ 'Europe/Bucharest',
+ 'Europe/Budapest',
+ 'Europe/Busingen',
+ 'Europe/Chisinau',
+ 'Europe/Copenhagen',
+ 'Europe/Dublin',
+ 'Europe/Gibraltar',
+ 'Europe/Guernsey',
+ 'Europe/Helsinki',
+ 'Europe/Isle_of_Man',
+ 'Europe/Istanbul',
+ 'Europe/Jersey',
+ 'Europe/Kaliningrad',
+ 'Europe/Kiev',
+ 'Europe/Kirov',
+ 'Europe/Lisbon',
+ 'Europe/Ljubljana',
+ 'Europe/London',
+ 'Europe/Luxembourg',
+ 'Europe/Madrid',
+ 'Europe/Malta',
+ 'Europe/Mariehamn',
+ 'Europe/Minsk',
+ 'Europe/Monaco',
+ 'Europe/Moscow',
+ 'Europe/Oslo',
+ 'Europe/Paris',
+ 'Europe/Podgorica',
+ 'Europe/Prague',
+ 'Europe/Riga',
+ 'Europe/Rome',
+ 'Europe/Samara',
+ 'Europe/San_Marino',
+ 'Europe/Sarajevo',
+ 'Europe/Saratov',
+ 'Europe/Simferopol',
+ 'Europe/Skopje',
+ 'Europe/Sofia',
+ 'Europe/Stockholm',
+ 'Europe/Tallinn',
+ 'Europe/Tirane',
+ 'Europe/Ulyanovsk',
+ 'Europe/Uzhgorod',
+ 'Europe/Vaduz',
+ 'Europe/Vatican',
+ 'Europe/Vienna',
+ 'Europe/Vilnius',
+ 'Europe/Volgograd',
+ 'Europe/Warsaw',
+ 'Europe/Zagreb',
+ 'Europe/Zaporozhye',
+ 'Europe/Zurich',
+ 'GMT',
+ 'Indian/Antananarivo',
+ 'Indian/Chagos',
+ 'Indian/Christmas',
+ 'Indian/Cocos',
+ 'Indian/Comoro',
+ 'Indian/Kerguelen',
+ 'Indian/Mahe',
+ 'Indian/Maldives',
+ 'Indian/Mauritius',
+ 'Indian/Mayotte',
+ 'Indian/Reunion',
+ 'Pacific/Apia',
+ 'Pacific/Auckland',
+ 'Pacific/Bougainville',
+ 'Pacific/Chatham',
+ 'Pacific/Chuuk',
+ 'Pacific/Easter',
+ 'Pacific/Efate',
+ 'Pacific/Fakaofo',
+ 'Pacific/Fiji',
+ 'Pacific/Funafuti',
+ 'Pacific/Galapagos',
+ 'Pacific/Gambier',
+ 'Pacific/Guadalcanal',
+ 'Pacific/Guam',
+ 'Pacific/Honolulu',
+ 'Pacific/Kanton',
+ 'Pacific/Kiritimati',
+ 'Pacific/Kosrae',
+ 'Pacific/Kwajalein',
+ 'Pacific/Majuro',
+ 'Pacific/Marquesas',
+ 'Pacific/Midway',
+ 'Pacific/Nauru',
+ 'Pacific/Niue',
+ 'Pacific/Norfolk',
+ 'Pacific/Noumea',
+ 'Pacific/Pago_Pago',
+ 'Pacific/Palau',
+ 'Pacific/Pitcairn',
+ 'Pacific/Pohnpei',
+ 'Pacific/Port_Moresby',
+ 'Pacific/Rarotonga',
+ 'Pacific/Saipan',
+ 'Pacific/Tahiti',
+ 'Pacific/Tarawa',
+ 'Pacific/Tongatapu',
+ 'Pacific/Wake',
+ 'Pacific/Wallis',
+ 'US/Alaska',
+ 'US/Arizona',
+ 'US/Central',
+ 'US/Eastern',
+ 'US/Hawaii',
+ 'US/Mountain',
+ 'US/Pacific',
+ 'UTC']
+common_timezones = LazyList(
+ tz for tz in common_timezones if tz in all_timezones)
+
+common_timezones_set = LazySet(common_timezones)
diff --git a/telegramer/include/pytz/exceptions.py b/telegramer/include/pytz/exceptions.py
new file mode 100644
index 0000000..4b20bde
--- /dev/null
+++ b/telegramer/include/pytz/exceptions.py
@@ -0,0 +1,59 @@
+'''
+Custom exceptions raised by pytz.
+'''
+
+__all__ = [
+ 'UnknownTimeZoneError', 'InvalidTimeError', 'AmbiguousTimeError',
+ 'NonExistentTimeError',
+]
+
+
+class Error(Exception):
+ '''Base class for all exceptions raised by the pytz library'''
+
+
+class UnknownTimeZoneError(KeyError, Error):
+ '''Exception raised when pytz is passed an unknown timezone.
+
+ >>> isinstance(UnknownTimeZoneError(), LookupError)
+ True
+
+ This class is actually a subclass of KeyError to provide backwards
+ compatibility with code relying on the undocumented behavior of earlier
+ pytz releases.
+
+ >>> isinstance(UnknownTimeZoneError(), KeyError)
+ True
+
+ And also a subclass of pytz.exceptions.Error, as are other pytz
+ exceptions.
+
+ >>> isinstance(UnknownTimeZoneError(), Error)
+ True
+
+ '''
+ pass
+
+
+class InvalidTimeError(Error):
+ '''Base class for invalid time exceptions.'''
+
+
+class AmbiguousTimeError(InvalidTimeError):
+ '''Exception raised when attempting to create an ambiguous wallclock time.
+
+ At the end of a DST transition period, a particular wallclock time will
+ occur twice (once before the clocks are set back, once after). Both
+ possibilities may be correct, unless further information is supplied.
+
+ See DstTzInfo.normalize() for more info
+ '''
+
+
+class NonExistentTimeError(InvalidTimeError):
+ '''Exception raised when attempting to create a wallclock time that
+ cannot exist.
+
+ At the start of a DST transition period, the wallclock time jumps forward.
+ The instants jumped over never occur.
+ '''
diff --git a/telegramer/include/pytz/lazy.py b/telegramer/include/pytz/lazy.py
new file mode 100644
index 0000000..39344fc
--- /dev/null
+++ b/telegramer/include/pytz/lazy.py
@@ -0,0 +1,172 @@
+from threading import RLock
+try:
+ from collections.abc import Mapping as DictMixin
+except ImportError: # Python < 3.3
+ try:
+ from UserDict import DictMixin # Python 2
+ except ImportError: # Python 3.0-3.3
+ from collections import Mapping as DictMixin
+
+
+# With lazy loading, we might end up with multiple threads triggering
+# it at the same time. We need a lock.
+_fill_lock = RLock()
+
+
+class LazyDict(DictMixin):
+ """Dictionary populated on first use."""
+ data = None
+
+ def __getitem__(self, key):
+ if self.data is None:
+ _fill_lock.acquire()
+ try:
+ if self.data is None:
+ self._fill()
+ finally:
+ _fill_lock.release()
+ return self.data[key.upper()]
+
+ def __contains__(self, key):
+ if self.data is None:
+ _fill_lock.acquire()
+ try:
+ if self.data is None:
+ self._fill()
+ finally:
+ _fill_lock.release()
+ return key in self.data
+
+ def __iter__(self):
+ if self.data is None:
+ _fill_lock.acquire()
+ try:
+ if self.data is None:
+ self._fill()
+ finally:
+ _fill_lock.release()
+ return iter(self.data)
+
+ def __len__(self):
+ if self.data is None:
+ _fill_lock.acquire()
+ try:
+ if self.data is None:
+ self._fill()
+ finally:
+ _fill_lock.release()
+ return len(self.data)
+
+ def keys(self):
+ if self.data is None:
+ _fill_lock.acquire()
+ try:
+ if self.data is None:
+ self._fill()
+ finally:
+ _fill_lock.release()
+ return self.data.keys()
+
+
+class LazyList(list):
+ """List populated on first use."""
+
+ _props = [
+ '__str__', '__repr__', '__unicode__',
+ '__hash__', '__sizeof__', '__cmp__',
+ '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__',
+ 'append', 'count', 'index', 'extend', 'insert', 'pop', 'remove',
+ 'reverse', 'sort', '__add__', '__radd__', '__iadd__', '__mul__',
+ '__rmul__', '__imul__', '__contains__', '__len__', '__nonzero__',
+ '__getitem__', '__setitem__', '__delitem__', '__iter__',
+ '__reversed__', '__getslice__', '__setslice__', '__delslice__']
+
+ def __new__(cls, fill_iter=None):
+
+ if fill_iter is None:
+ return list()
+
+ # We need a new class as we will be dynamically messing with its
+ # methods.
+ class LazyList(list):
+ pass
+
+ fill_iter = [fill_iter]
+
+ def lazy(name):
+ def _lazy(self, *args, **kw):
+ _fill_lock.acquire()
+ try:
+ if len(fill_iter) > 0:
+ list.extend(self, fill_iter.pop())
+ for method_name in cls._props:
+ delattr(LazyList, method_name)
+ finally:
+ _fill_lock.release()
+ return getattr(list, name)(self, *args, **kw)
+ return _lazy
+
+ for name in cls._props:
+ setattr(LazyList, name, lazy(name))
+
+ new_list = LazyList()
+ return new_list
+
+# Not all versions of Python declare the same magic methods.
+# Filter out properties that don't exist in this version of Python
+# from the list.
+LazyList._props = [prop for prop in LazyList._props if hasattr(list, prop)]
+
+
+class LazySet(set):
+ """Set populated on first use."""
+
+ _props = (
+ '__str__', '__repr__', '__unicode__',
+ '__hash__', '__sizeof__', '__cmp__',
+ '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__',
+ '__contains__', '__len__', '__nonzero__',
+ '__getitem__', '__setitem__', '__delitem__', '__iter__',
+ '__sub__', '__and__', '__xor__', '__or__',
+ '__rsub__', '__rand__', '__rxor__', '__ror__',
+ '__isub__', '__iand__', '__ixor__', '__ior__',
+ 'add', 'clear', 'copy', 'difference', 'difference_update',
+ 'discard', 'intersection', 'intersection_update', 'isdisjoint',
+ 'issubset', 'issuperset', 'pop', 'remove',
+ 'symmetric_difference', 'symmetric_difference_update',
+ 'union', 'update')
+
+ def __new__(cls, fill_iter=None):
+
+ if fill_iter is None:
+ return set()
+
+ class LazySet(set):
+ pass
+
+ fill_iter = [fill_iter]
+
+ def lazy(name):
+ def _lazy(self, *args, **kw):
+ _fill_lock.acquire()
+ try:
+ if len(fill_iter) > 0:
+ for i in fill_iter.pop():
+ set.add(self, i)
+ for method_name in cls._props:
+ delattr(LazySet, method_name)
+ finally:
+ _fill_lock.release()
+ return getattr(set, name)(self, *args, **kw)
+ return _lazy
+
+ for name in cls._props:
+ setattr(LazySet, name, lazy(name))
+
+ new_set = LazySet()
+ return new_set
+
+# Not all versions of Python declare the same magic methods.
+# Filter out properties that don't exist in this version of Python
+# from the list.
+LazySet._props = [prop for prop in LazySet._props if hasattr(set, prop)]
diff --git a/telegramer/include/pytz/reference.py b/telegramer/include/pytz/reference.py
new file mode 100644
index 0000000..f765ca0
--- /dev/null
+++ b/telegramer/include/pytz/reference.py
@@ -0,0 +1,140 @@
+'''
+Reference tzinfo implementations from the Python docs.
+Used for testing against as they are only correct for the years
+1987 to 2006. Do not use these for real code.
+'''
+
+from datetime import tzinfo, timedelta, datetime
+from pytz import HOUR, ZERO, UTC
+
+__all__ = [
+ 'FixedOffset',
+ 'LocalTimezone',
+ 'USTimeZone',
+ 'Eastern',
+ 'Central',
+ 'Mountain',
+ 'Pacific',
+ 'UTC'
+]
+
+
+# A class building tzinfo objects for fixed-offset time zones.
+# Note that FixedOffset(0, "UTC") is a different way to build a
+# UTC tzinfo object.
+class FixedOffset(tzinfo):
+ """Fixed offset in minutes east from UTC."""
+
+ def __init__(self, offset, name):
+ self.__offset = timedelta(minutes=offset)
+ self.__name = name
+
+ def utcoffset(self, dt):
+ return self.__offset
+
+ def tzname(self, dt):
+ return self.__name
+
+ def dst(self, dt):
+ return ZERO
+
+
+import time as _time
+
+STDOFFSET = timedelta(seconds=-_time.timezone)
+if _time.daylight:
+ DSTOFFSET = timedelta(seconds=-_time.altzone)
+else:
+ DSTOFFSET = STDOFFSET
+
+DSTDIFF = DSTOFFSET - STDOFFSET
+
+
+# A class capturing the platform's idea of local time.
+class LocalTimezone(tzinfo):
+
+ def utcoffset(self, dt):
+ if self._isdst(dt):
+ return DSTOFFSET
+ else:
+ return STDOFFSET
+
+ def dst(self, dt):
+ if self._isdst(dt):
+ return DSTDIFF
+ else:
+ return ZERO
+
+ def tzname(self, dt):
+ return _time.tzname[self._isdst(dt)]
+
+ def _isdst(self, dt):
+ tt = (dt.year, dt.month, dt.day,
+ dt.hour, dt.minute, dt.second,
+ dt.weekday(), 0, -1)
+ stamp = _time.mktime(tt)
+ tt = _time.localtime(stamp)
+ return tt.tm_isdst > 0
+
+Local = LocalTimezone()
+
+
+def first_sunday_on_or_after(dt):
+ days_to_go = 6 - dt.weekday()
+ if days_to_go:
+ dt += timedelta(days_to_go)
+ return dt
+
+
+# In the US, DST starts at 2am (standard time) on the first Sunday in April.
+DSTSTART = datetime(1, 4, 1, 2)
+# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
+# which is the first Sunday on or after Oct 25.
+DSTEND = datetime(1, 10, 25, 1)
+
+
+# A complete implementation of current DST rules for major US time zones.
+class USTimeZone(tzinfo):
+
+ def __init__(self, hours, reprname, stdname, dstname):
+ self.stdoffset = timedelta(hours=hours)
+ self.reprname = reprname
+ self.stdname = stdname
+ self.dstname = dstname
+
+ def __repr__(self):
+ return self.reprname
+
+ def tzname(self, dt):
+ if self.dst(dt):
+ return self.dstname
+ else:
+ return self.stdname
+
+ def utcoffset(self, dt):
+ return self.stdoffset + self.dst(dt)
+
+ def dst(self, dt):
+ if dt is None or dt.tzinfo is None:
+ # An exception may be sensible here, in one or both cases.
+ # It depends on how you want to treat them. The default
+ # fromutc() implementation (called by the default astimezone()
+ # implementation) passes a datetime with dt.tzinfo is self.
+ return ZERO
+ assert dt.tzinfo is self
+
+ # Find first Sunday in April & the last in October.
+ start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
+ end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
+
+ # Can't compare naive to aware objects, so strip the timezone from
+ # dt first.
+ if start <= dt.replace(tzinfo=None) < end:
+ return HOUR
+ else:
+ return ZERO
+
+Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
+Central = USTimeZone(-6, "Central", "CST", "CDT")
+Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
+Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
diff --git a/telegramer/include/pytz/tzfile.py b/telegramer/include/pytz/tzfile.py
new file mode 100644
index 0000000..99e7448
--- /dev/null
+++ b/telegramer/include/pytz/tzfile.py
@@ -0,0 +1,133 @@
+'''
+$Id: tzfile.py,v 1.8 2004/06/03 00:15:24 zenzen Exp $
+'''
+
+from datetime import datetime
+from struct import unpack, calcsize
+
+from pytz.tzinfo import StaticTzInfo, DstTzInfo, memorized_ttinfo
+from pytz.tzinfo import memorized_datetime, memorized_timedelta
+
+
+def _byte_string(s):
+ """Cast a string or byte string to an ASCII byte string."""
+ return s.encode('ASCII')
+
+_NULL = _byte_string('\0')
+
+
+def _std_string(s):
+ """Cast a string or byte string to an ASCII string."""
+ return str(s.decode('ASCII'))
+
+
+def build_tzinfo(zone, fp):
+ head_fmt = '>4s c 15x 6l'
+ head_size = calcsize(head_fmt)
+ (magic, format, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt,
+ typecnt, charcnt) = unpack(head_fmt, fp.read(head_size))
+
+ # Make sure it is a tzfile(5) file
+ assert magic == _byte_string('TZif'), 'Got magic %s' % repr(magic)
+
+ # Read out the transition times, localtime indices and ttinfo structures.
+ data_fmt = '>%(timecnt)dl %(timecnt)dB %(ttinfo)s %(charcnt)ds' % dict(
+ timecnt=timecnt, ttinfo='lBB' * typecnt, charcnt=charcnt)
+ data_size = calcsize(data_fmt)
+ data = unpack(data_fmt, fp.read(data_size))
+
+ # make sure we unpacked the right number of values
+ assert len(data) == 2 * timecnt + 3 * typecnt + 1
+ transitions = [memorized_datetime(trans)
+ for trans in data[:timecnt]]
+ lindexes = list(data[timecnt:2 * timecnt])
+ ttinfo_raw = data[2 * timecnt:-1]
+ tznames_raw = data[-1]
+ del data
+
+ # Process ttinfo into separate structs
+ ttinfo = []
+ tznames = {}
+ i = 0
+ while i < len(ttinfo_raw):
+ # have we looked up this timezone name yet?
+ tzname_offset = ttinfo_raw[i + 2]
+ if tzname_offset not in tznames:
+ nul = tznames_raw.find(_NULL, tzname_offset)
+ if nul < 0:
+ nul = len(tznames_raw)
+ tznames[tzname_offset] = _std_string(
+ tznames_raw[tzname_offset:nul])
+ ttinfo.append((ttinfo_raw[i],
+ bool(ttinfo_raw[i + 1]),
+ tznames[tzname_offset]))
+ i += 3
+
+ # Now build the timezone object
+ if len(ttinfo) == 1 or len(transitions) == 0:
+ ttinfo[0][0], ttinfo[0][2]
+ cls = type(zone, (StaticTzInfo,), dict(
+ zone=zone,
+ _utcoffset=memorized_timedelta(ttinfo[0][0]),
+ _tzname=ttinfo[0][2]))
+ else:
+ # Early dates use the first standard time ttinfo
+ i = 0
+ while ttinfo[i][1]:
+ i += 1
+ if ttinfo[i] == ttinfo[lindexes[0]]:
+ transitions[0] = datetime.min
+ else:
+ transitions.insert(0, datetime.min)
+ lindexes.insert(0, i)
+
+ # calculate transition info
+ transition_info = []
+ for i in range(len(transitions)):
+ inf = ttinfo[lindexes[i]]
+ utcoffset = inf[0]
+ if not inf[1]:
+ dst = 0
+ else:
+ for j in range(i - 1, -1, -1):
+ prev_inf = ttinfo[lindexes[j]]
+ if not prev_inf[1]:
+ break
+ dst = inf[0] - prev_inf[0] # dst offset
+
+ # Bad dst? Look further. DST > 24 hours happens when
+ # a timzone has moved across the international dateline.
+ if dst <= 0 or dst > 3600 * 3:
+ for j in range(i + 1, len(transitions)):
+ stdinf = ttinfo[lindexes[j]]
+ if not stdinf[1]:
+ dst = inf[0] - stdinf[0]
+ if dst > 0:
+ break # Found a useful std time.
+
+ tzname = inf[2]
+
+ # Round utcoffset and dst to the nearest minute or the
+ # datetime library will complain. Conversions to these timezones
+ # might be up to plus or minus 30 seconds out, but it is
+ # the best we can do.
+ utcoffset = int((utcoffset + 30) // 60) * 60
+ dst = int((dst + 30) // 60) * 60
+ transition_info.append(memorized_ttinfo(utcoffset, dst, tzname))
+
+ cls = type(zone, (DstTzInfo,), dict(
+ zone=zone,
+ _utc_transition_times=transitions,
+ _transition_info=transition_info))
+
+ return cls()
+
+if __name__ == '__main__':
+ import os.path
+ from pprint import pprint
+ base = os.path.join(os.path.dirname(__file__), 'zoneinfo')
+ tz = build_tzinfo('Australia/Melbourne',
+ open(os.path.join(base, 'Australia', 'Melbourne'), 'rb'))
+ tz = build_tzinfo('US/Eastern',
+ open(os.path.join(base, 'US', 'Eastern'), 'rb'))
+ pprint(tz._utc_transition_times)
diff --git a/telegramer/include/pytz/tzinfo.py b/telegramer/include/pytz/tzinfo.py
new file mode 100644
index 0000000..725978d
--- /dev/null
+++ b/telegramer/include/pytz/tzinfo.py
@@ -0,0 +1,577 @@
+'''Base classes and helpers for building zone specific tzinfo classes'''
+
+from datetime import datetime, timedelta, tzinfo
+from bisect import bisect_right
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+import pytz
+from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError
+
+__all__ = []
+
+_timedelta_cache = {}
+
+
+def memorized_timedelta(seconds):
+ '''Create only one instance of each distinct timedelta'''
+ try:
+ return _timedelta_cache[seconds]
+ except KeyError:
+ delta = timedelta(seconds=seconds)
+ _timedelta_cache[seconds] = delta
+ return delta
+
+_epoch = datetime.utcfromtimestamp(0)
+_datetime_cache = {0: _epoch}
+
+
+def memorized_datetime(seconds):
+ '''Create only one instance of each distinct datetime'''
+ try:
+ return _datetime_cache[seconds]
+ except KeyError:
+ # NB. We can't just do datetime.utcfromtimestamp(seconds) as this
+ # fails with negative values under Windows (Bug #90096)
+ dt = _epoch + timedelta(seconds=seconds)
+ _datetime_cache[seconds] = dt
+ return dt
+
+_ttinfo_cache = {}
+
+
+def memorized_ttinfo(*args):
+ '''Create only one instance of each distinct tuple'''
+ try:
+ return _ttinfo_cache[args]
+ except KeyError:
+ ttinfo = (
+ memorized_timedelta(args[0]),
+ memorized_timedelta(args[1]),
+ args[2]
+ )
+ _ttinfo_cache[args] = ttinfo
+ return ttinfo
+
+_notime = memorized_timedelta(0)
+
+
+def _to_seconds(td):
+ '''Convert a timedelta to seconds'''
+ return td.seconds + td.days * 24 * 60 * 60
+
+
+class BaseTzInfo(tzinfo):
+ # Overridden in subclass
+ _utcoffset = None
+ _tzname = None
+ zone = None
+
+ def __str__(self):
+ return self.zone
+
+
+class StaticTzInfo(BaseTzInfo):
+ '''A timezone that has a constant offset from UTC
+
+ These timezones are rare, as most locations have changed their
+ offset at some point in their history
+ '''
+ def fromutc(self, dt):
+ '''See datetime.tzinfo.fromutc'''
+ if dt.tzinfo is not None and dt.tzinfo is not self:
+ raise ValueError('fromutc: dt.tzinfo is not self')
+ return (dt + self._utcoffset).replace(tzinfo=self)
+
+ def utcoffset(self, dt, is_dst=None):
+ '''See datetime.tzinfo.utcoffset
+
+ is_dst is ignored for StaticTzInfo, and exists only to
+ retain compatibility with DstTzInfo.
+ '''
+ return self._utcoffset
+
+ def dst(self, dt, is_dst=None):
+ '''See datetime.tzinfo.dst
+
+ is_dst is ignored for StaticTzInfo, and exists only to
+ retain compatibility with DstTzInfo.
+ '''
+ return _notime
+
+ def tzname(self, dt, is_dst=None):
+ '''See datetime.tzinfo.tzname
+
+ is_dst is ignored for StaticTzInfo, and exists only to
+ retain compatibility with DstTzInfo.
+ '''
+ return self._tzname
+
+ def localize(self, dt, is_dst=False):
+ '''Convert naive time to local time'''
+ if dt.tzinfo is not None:
+ raise ValueError('Not naive datetime (tzinfo is already set)')
+ return dt.replace(tzinfo=self)
+
+ def normalize(self, dt, is_dst=False):
+ '''Correct the timezone information on the given datetime.
+
+ This is normally a no-op, as StaticTzInfo timezones never have
+ ambiguous cases to correct:
+
+ >>> from pytz import timezone
+ >>> gmt = timezone('GMT')
+ >>> isinstance(gmt, StaticTzInfo)
+ True
+ >>> dt = datetime(2011, 5, 8, 1, 2, 3, tzinfo=gmt)
+ >>> gmt.normalize(dt) is dt
+ True
+
+ The supported method of converting between timezones is to use
+ datetime.astimezone(). Currently normalize() also works:
+
+ >>> la = timezone('America/Los_Angeles')
+ >>> dt = la.localize(datetime(2011, 5, 7, 1, 2, 3))
+ >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
+ >>> gmt.normalize(dt).strftime(fmt)
+ '2011-05-07 08:02:03 GMT (+0000)'
+ '''
+ if dt.tzinfo is self:
+ return dt
+ if dt.tzinfo is None:
+ raise ValueError('Naive time - no tzinfo set')
+ return dt.astimezone(self)
+
+ def __repr__(self):
+ return '' % (self.zone,)
+
+ def __reduce__(self):
+ # Special pickle to zone remains a singleton and to cope with
+ # database changes.
+ return pytz._p, (self.zone,)
+
+
+class DstTzInfo(BaseTzInfo):
+ '''A timezone that has a variable offset from UTC
+
+ The offset might change if daylight saving time comes into effect,
+ or at a point in history when the region decides to change their
+ timezone definition.
+ '''
+ # Overridden in subclass
+
+ # Sorted list of DST transition times, UTC
+ _utc_transition_times = None
+
+ # [(utcoffset, dstoffset, tzname)] corresponding to
+ # _utc_transition_times entries
+ _transition_info = None
+
+ zone = None
+
+ # Set in __init__
+
+ _tzinfos = None
+ _dst = None # DST offset
+
+ def __init__(self, _inf=None, _tzinfos=None):
+ if _inf:
+ self._tzinfos = _tzinfos
+ self._utcoffset, self._dst, self._tzname = _inf
+ else:
+ _tzinfos = {}
+ self._tzinfos = _tzinfos
+ self._utcoffset, self._dst, self._tzname = (
+ self._transition_info[0])
+ _tzinfos[self._transition_info[0]] = self
+ for inf in self._transition_info[1:]:
+ if inf not in _tzinfos:
+ _tzinfos[inf] = self.__class__(inf, _tzinfos)
+
+ def fromutc(self, dt):
+ '''See datetime.tzinfo.fromutc'''
+ if (dt.tzinfo is not None and
+ getattr(dt.tzinfo, '_tzinfos', None) is not self._tzinfos):
+ raise ValueError('fromutc: dt.tzinfo is not self')
+ dt = dt.replace(tzinfo=None)
+ idx = max(0, bisect_right(self._utc_transition_times, dt) - 1)
+ inf = self._transition_info[idx]
+ return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf])
+
+ def normalize(self, dt):
+ '''Correct the timezone information on the given datetime
+
+ If date arithmetic crosses DST boundaries, the tzinfo
+ is not magically adjusted. This method normalizes the
+ tzinfo to the correct one.
+
+ To test, first we need to do some setup
+
+ >>> from pytz import timezone
+ >>> utc = timezone('UTC')
+ >>> eastern = timezone('US/Eastern')
+ >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
+
+ We next create a datetime right on an end-of-DST transition point,
+ the instant when the wallclocks are wound back one hour.
+
+ >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)
+ >>> loc_dt = utc_dt.astimezone(eastern)
+ >>> loc_dt.strftime(fmt)
+ '2002-10-27 01:00:00 EST (-0500)'
+
+ Now, if we subtract a few minutes from it, note that the timezone
+ information has not changed.
+
+ >>> before = loc_dt - timedelta(minutes=10)
+ >>> before.strftime(fmt)
+ '2002-10-27 00:50:00 EST (-0500)'
+
+ But we can fix that by calling the normalize method
+
+ >>> before = eastern.normalize(before)
+ >>> before.strftime(fmt)
+ '2002-10-27 01:50:00 EDT (-0400)'
+
+ The supported method of converting between timezones is to use
+ datetime.astimezone(). Currently, normalize() also works:
+
+ >>> th = timezone('Asia/Bangkok')
+ >>> am = timezone('Europe/Amsterdam')
+ >>> dt = th.localize(datetime(2011, 5, 7, 1, 2, 3))
+ >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
+ >>> am.normalize(dt).strftime(fmt)
+ '2011-05-06 20:02:03 CEST (+0200)'
+ '''
+ if dt.tzinfo is None:
+ raise ValueError('Naive time - no tzinfo set')
+
+ # Convert dt in localtime to UTC
+ offset = dt.tzinfo._utcoffset
+ dt = dt.replace(tzinfo=None)
+ dt = dt - offset
+ # convert it back, and return it
+ return self.fromutc(dt)
+
+ def localize(self, dt, is_dst=False):
+ '''Convert naive time to local time.
+
+ This method should be used to construct localtimes, rather
+ than passing a tzinfo argument to a datetime constructor.
+
+ is_dst is used to determine the correct timezone in the ambigous
+ period at the end of daylight saving time.
+
+ >>> from pytz import timezone
+ >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
+ >>> amdam = timezone('Europe/Amsterdam')
+ >>> dt = datetime(2004, 10, 31, 2, 0, 0)
+ >>> loc_dt1 = amdam.localize(dt, is_dst=True)
+ >>> loc_dt2 = amdam.localize(dt, is_dst=False)
+ >>> loc_dt1.strftime(fmt)
+ '2004-10-31 02:00:00 CEST (+0200)'
+ >>> loc_dt2.strftime(fmt)
+ '2004-10-31 02:00:00 CET (+0100)'
+ >>> str(loc_dt2 - loc_dt1)
+ '1:00:00'
+
+ Use is_dst=None to raise an AmbiguousTimeError for ambiguous
+ times at the end of daylight saving time
+
+ >>> try:
+ ... loc_dt1 = amdam.localize(dt, is_dst=None)
+ ... except AmbiguousTimeError:
+ ... print('Ambiguous')
+ Ambiguous
+
+ is_dst defaults to False
+
+ >>> amdam.localize(dt) == amdam.localize(dt, False)
+ True
+
+ is_dst is also used to determine the correct timezone in the
+ wallclock times jumped over at the start of daylight saving time.
+
+ >>> pacific = timezone('US/Pacific')
+ >>> dt = datetime(2008, 3, 9, 2, 0, 0)
+ >>> ploc_dt1 = pacific.localize(dt, is_dst=True)
+ >>> ploc_dt2 = pacific.localize(dt, is_dst=False)
+ >>> ploc_dt1.strftime(fmt)
+ '2008-03-09 02:00:00 PDT (-0700)'
+ >>> ploc_dt2.strftime(fmt)
+ '2008-03-09 02:00:00 PST (-0800)'
+ >>> str(ploc_dt2 - ploc_dt1)
+ '1:00:00'
+
+ Use is_dst=None to raise a NonExistentTimeError for these skipped
+ times.
+
+ >>> try:
+ ... loc_dt1 = pacific.localize(dt, is_dst=None)
+ ... except NonExistentTimeError:
+ ... print('Non-existent')
+ Non-existent
+ '''
+ if dt.tzinfo is not None:
+ raise ValueError('Not naive datetime (tzinfo is already set)')
+
+ # Find the two best possibilities.
+ possible_loc_dt = set()
+ for delta in [timedelta(days=-1), timedelta(days=1)]:
+ loc_dt = dt + delta
+ idx = max(0, bisect_right(
+ self._utc_transition_times, loc_dt) - 1)
+ inf = self._transition_info[idx]
+ tzinfo = self._tzinfos[inf]
+ loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo))
+ if loc_dt.replace(tzinfo=None) == dt:
+ possible_loc_dt.add(loc_dt)
+
+ if len(possible_loc_dt) == 1:
+ return possible_loc_dt.pop()
+
+ # If there are no possibly correct timezones, we are attempting
+ # to convert a time that never happened - the time period jumped
+ # during the start-of-DST transition period.
+ if len(possible_loc_dt) == 0:
+ # If we refuse to guess, raise an exception.
+ if is_dst is None:
+ raise NonExistentTimeError(dt)
+
+ # If we are forcing the pre-DST side of the DST transition, we
+ # obtain the correct timezone by winding the clock forward a few
+ # hours.
+ elif is_dst:
+ return self.localize(
+ dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6)
+
+ # If we are forcing the post-DST side of the DST transition, we
+ # obtain the correct timezone by winding the clock back.
+ else:
+ return self.localize(
+ dt - timedelta(hours=6),
+ is_dst=False) + timedelta(hours=6)
+
+ # If we get this far, we have multiple possible timezones - this
+ # is an ambiguous case occuring during the end-of-DST transition.
+
+ # If told to be strict, raise an exception since we have an
+ # ambiguous case
+ if is_dst is None:
+ raise AmbiguousTimeError(dt)
+
+ # Filter out the possiblilities that don't match the requested
+ # is_dst
+ filtered_possible_loc_dt = [
+ p for p in possible_loc_dt if bool(p.tzinfo._dst) == is_dst
+ ]
+
+ # Hopefully we only have one possibility left. Return it.
+ if len(filtered_possible_loc_dt) == 1:
+ return filtered_possible_loc_dt[0]
+
+ if len(filtered_possible_loc_dt) == 0:
+ filtered_possible_loc_dt = list(possible_loc_dt)
+
+ # If we get this far, we have in a wierd timezone transition
+ # where the clocks have been wound back but is_dst is the same
+ # in both (eg. Europe/Warsaw 1915 when they switched to CET).
+ # At this point, we just have to guess unless we allow more
+ # hints to be passed in (such as the UTC offset or abbreviation),
+ # but that is just getting silly.
+ #
+ # Choose the earliest (by UTC) applicable timezone if is_dst=True
+ # Choose the latest (by UTC) applicable timezone if is_dst=False
+ # i.e., behave like end-of-DST transition
+ dates = {} # utc -> local
+ for local_dt in filtered_possible_loc_dt:
+ utc_time = (
+ local_dt.replace(tzinfo=None) - local_dt.tzinfo._utcoffset)
+ assert utc_time not in dates
+ dates[utc_time] = local_dt
+ return dates[[min, max][not is_dst](dates)]
+
+ def utcoffset(self, dt, is_dst=None):
+ '''See datetime.tzinfo.utcoffset
+
+ The is_dst parameter may be used to remove ambiguity during DST
+ transitions.
+
+ >>> from pytz import timezone
+ >>> tz = timezone('America/St_Johns')
+ >>> ambiguous = datetime(2009, 10, 31, 23, 30)
+
+ >>> str(tz.utcoffset(ambiguous, is_dst=False))
+ '-1 day, 20:30:00'
+
+ >>> str(tz.utcoffset(ambiguous, is_dst=True))
+ '-1 day, 21:30:00'
+
+ >>> try:
+ ... tz.utcoffset(ambiguous)
+ ... except AmbiguousTimeError:
+ ... print('Ambiguous')
+ Ambiguous
+
+ '''
+ if dt is None:
+ return None
+ elif dt.tzinfo is not self:
+ dt = self.localize(dt, is_dst)
+ return dt.tzinfo._utcoffset
+ else:
+ return self._utcoffset
+
+ def dst(self, dt, is_dst=None):
+ '''See datetime.tzinfo.dst
+
+ The is_dst parameter may be used to remove ambiguity during DST
+ transitions.
+
+ >>> from pytz import timezone
+ >>> tz = timezone('America/St_Johns')
+
+ >>> normal = datetime(2009, 9, 1)
+
+ >>> str(tz.dst(normal))
+ '1:00:00'
+ >>> str(tz.dst(normal, is_dst=False))
+ '1:00:00'
+ >>> str(tz.dst(normal, is_dst=True))
+ '1:00:00'
+
+ >>> ambiguous = datetime(2009, 10, 31, 23, 30)
+
+ >>> str(tz.dst(ambiguous, is_dst=False))
+ '0:00:00'
+ >>> str(tz.dst(ambiguous, is_dst=True))
+ '1:00:00'
+ >>> try:
+ ... tz.dst(ambiguous)
+ ... except AmbiguousTimeError:
+ ... print('Ambiguous')
+ Ambiguous
+
+ '''
+ if dt is None:
+ return None
+ elif dt.tzinfo is not self:
+ dt = self.localize(dt, is_dst)
+ return dt.tzinfo._dst
+ else:
+ return self._dst
+
+ def tzname(self, dt, is_dst=None):
+ '''See datetime.tzinfo.tzname
+
+ The is_dst parameter may be used to remove ambiguity during DST
+ transitions.
+
+ >>> from pytz import timezone
+ >>> tz = timezone('America/St_Johns')
+
+ >>> normal = datetime(2009, 9, 1)
+
+ >>> tz.tzname(normal)
+ 'NDT'
+ >>> tz.tzname(normal, is_dst=False)
+ 'NDT'
+ >>> tz.tzname(normal, is_dst=True)
+ 'NDT'
+
+ >>> ambiguous = datetime(2009, 10, 31, 23, 30)
+
+ >>> tz.tzname(ambiguous, is_dst=False)
+ 'NST'
+ >>> tz.tzname(ambiguous, is_dst=True)
+ 'NDT'
+ >>> try:
+ ... tz.tzname(ambiguous)
+ ... except AmbiguousTimeError:
+ ... print('Ambiguous')
+ Ambiguous
+ '''
+ if dt is None:
+ return self.zone
+ elif dt.tzinfo is not self:
+ dt = self.localize(dt, is_dst)
+ return dt.tzinfo._tzname
+ else:
+ return self._tzname
+
+ def __repr__(self):
+ if self._dst:
+ dst = 'DST'
+ else:
+ dst = 'STD'
+ if self._utcoffset > _notime:
+ return '' % (
+ self.zone, self._tzname, self._utcoffset, dst
+ )
+ else:
+ return '' % (
+ self.zone, self._tzname, self._utcoffset, dst
+ )
+
+ def __reduce__(self):
+ # Special pickle to zone remains a singleton and to cope with
+ # database changes.
+ return pytz._p, (
+ self.zone,
+ _to_seconds(self._utcoffset),
+ _to_seconds(self._dst),
+ self._tzname
+ )
+
+
+def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
+ """Factory function for unpickling pytz tzinfo instances.
+
+ This is shared for both StaticTzInfo and DstTzInfo instances, because
+ database changes could cause a zones implementation to switch between
+ these two base classes and we can't break pickles on a pytz version
+ upgrade.
+ """
+ # Raises a KeyError if zone no longer exists, which should never happen
+ # and would be a bug.
+ tz = pytz.timezone(zone)
+
+ # A StaticTzInfo - just return it
+ if utcoffset is None:
+ return tz
+
+ # This pickle was created from a DstTzInfo. We need to
+ # determine which of the list of tzinfo instances for this zone
+ # to use in order to restore the state of any datetime instances using
+ # it correctly.
+ utcoffset = memorized_timedelta(utcoffset)
+ dstoffset = memorized_timedelta(dstoffset)
+ try:
+ return tz._tzinfos[(utcoffset, dstoffset, tzname)]
+ except KeyError:
+ # The particular state requested in this timezone no longer exists.
+ # This indicates a corrupt pickle, or the timezone database has been
+ # corrected violently enough to make this particular
+ # (utcoffset,dstoffset) no longer exist in the zone, or the
+ # abbreviation has been changed.
+ pass
+
+ # See if we can find an entry differing only by tzname. Abbreviations
+ # get changed from the initial guess by the database maintainers to
+ # match reality when this information is discovered.
+ for localized_tz in tz._tzinfos.values():
+ if (localized_tz._utcoffset == utcoffset and
+ localized_tz._dst == dstoffset):
+ return localized_tz
+
+ # This (utcoffset, dstoffset) information has been removed from the
+ # zone. Add it back. This might occur when the database maintainers have
+ # corrected incorrect information. datetime instances using this
+ # incorrect information will continue to do so, exactly as they were
+ # before being pickled. This is purely an overly paranoid safety net - I
+ # doubt this will ever been needed in real life.
+ inf = (utcoffset, dstoffset, tzname)
+ tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos)
+ return tz._tzinfos[inf]
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Abidjan b/telegramer/include/pytz/zoneinfo/Africa/Abidjan
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Abidjan differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Accra b/telegramer/include/pytz/zoneinfo/Africa/Accra
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Accra differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Addis_Ababa b/telegramer/include/pytz/zoneinfo/Africa/Addis_Ababa
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Addis_Ababa differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Algiers b/telegramer/include/pytz/zoneinfo/Africa/Algiers
new file mode 100644
index 0000000..6cfd8a1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Algiers differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Asmara b/telegramer/include/pytz/zoneinfo/Africa/Asmara
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Asmara differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Asmera b/telegramer/include/pytz/zoneinfo/Africa/Asmera
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Asmera differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Bamako b/telegramer/include/pytz/zoneinfo/Africa/Bamako
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Bamako differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Bangui b/telegramer/include/pytz/zoneinfo/Africa/Bangui
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Bangui differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Banjul b/telegramer/include/pytz/zoneinfo/Africa/Banjul
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Banjul differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Bissau b/telegramer/include/pytz/zoneinfo/Africa/Bissau
new file mode 100644
index 0000000..82ea5aa
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Bissau differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Blantyre b/telegramer/include/pytz/zoneinfo/Africa/Blantyre
new file mode 100644
index 0000000..52753c0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Blantyre differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Brazzaville b/telegramer/include/pytz/zoneinfo/Africa/Brazzaville
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Brazzaville differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Bujumbura b/telegramer/include/pytz/zoneinfo/Africa/Bujumbura
new file mode 100644
index 0000000..52753c0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Bujumbura differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Cairo b/telegramer/include/pytz/zoneinfo/Africa/Cairo
new file mode 100644
index 0000000..d3f8196
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Cairo differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Casablanca b/telegramer/include/pytz/zoneinfo/Africa/Casablanca
new file mode 100644
index 0000000..17e0d1b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Casablanca differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Ceuta b/telegramer/include/pytz/zoneinfo/Africa/Ceuta
new file mode 100644
index 0000000..850c8f0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Ceuta differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Conakry b/telegramer/include/pytz/zoneinfo/Africa/Conakry
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Conakry differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Dakar b/telegramer/include/pytz/zoneinfo/Africa/Dakar
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Dakar differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Dar_es_Salaam b/telegramer/include/pytz/zoneinfo/Africa/Dar_es_Salaam
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Dar_es_Salaam differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Djibouti b/telegramer/include/pytz/zoneinfo/Africa/Djibouti
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Djibouti differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Douala b/telegramer/include/pytz/zoneinfo/Africa/Douala
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Douala differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/El_Aaiun b/telegramer/include/pytz/zoneinfo/Africa/El_Aaiun
new file mode 100644
index 0000000..64f1b76
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/El_Aaiun differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Freetown b/telegramer/include/pytz/zoneinfo/Africa/Freetown
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Freetown differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Gaborone b/telegramer/include/pytz/zoneinfo/Africa/Gaborone
new file mode 100644
index 0000000..52753c0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Gaborone differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Harare b/telegramer/include/pytz/zoneinfo/Africa/Harare
new file mode 100644
index 0000000..52753c0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Harare differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Johannesburg b/telegramer/include/pytz/zoneinfo/Africa/Johannesburg
new file mode 100644
index 0000000..b1c425d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Johannesburg differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Juba b/telegramer/include/pytz/zoneinfo/Africa/Juba
new file mode 100644
index 0000000..0648294
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Juba differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Kampala b/telegramer/include/pytz/zoneinfo/Africa/Kampala
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Kampala differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Khartoum b/telegramer/include/pytz/zoneinfo/Africa/Khartoum
new file mode 100644
index 0000000..8ee8cb9
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Khartoum differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Kigali b/telegramer/include/pytz/zoneinfo/Africa/Kigali
new file mode 100644
index 0000000..52753c0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Kigali differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Kinshasa b/telegramer/include/pytz/zoneinfo/Africa/Kinshasa
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Kinshasa differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Lagos b/telegramer/include/pytz/zoneinfo/Africa/Lagos
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Lagos differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Libreville b/telegramer/include/pytz/zoneinfo/Africa/Libreville
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Libreville differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Lome b/telegramer/include/pytz/zoneinfo/Africa/Lome
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Lome differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Luanda b/telegramer/include/pytz/zoneinfo/Africa/Luanda
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Luanda differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Lubumbashi b/telegramer/include/pytz/zoneinfo/Africa/Lubumbashi
new file mode 100644
index 0000000..52753c0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Lubumbashi differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Lusaka b/telegramer/include/pytz/zoneinfo/Africa/Lusaka
new file mode 100644
index 0000000..52753c0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Lusaka differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Malabo b/telegramer/include/pytz/zoneinfo/Africa/Malabo
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Malabo differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Maputo b/telegramer/include/pytz/zoneinfo/Africa/Maputo
new file mode 100644
index 0000000..52753c0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Maputo differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Maseru b/telegramer/include/pytz/zoneinfo/Africa/Maseru
new file mode 100644
index 0000000..b1c425d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Maseru differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Mbabane b/telegramer/include/pytz/zoneinfo/Africa/Mbabane
new file mode 100644
index 0000000..b1c425d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Mbabane differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Mogadishu b/telegramer/include/pytz/zoneinfo/Africa/Mogadishu
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Mogadishu differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Monrovia b/telegramer/include/pytz/zoneinfo/Africa/Monrovia
new file mode 100644
index 0000000..6d68850
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Monrovia differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Nairobi b/telegramer/include/pytz/zoneinfo/Africa/Nairobi
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Nairobi differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Ndjamena b/telegramer/include/pytz/zoneinfo/Africa/Ndjamena
new file mode 100644
index 0000000..a968845
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Ndjamena differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Niamey b/telegramer/include/pytz/zoneinfo/Africa/Niamey
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Niamey differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Nouakchott b/telegramer/include/pytz/zoneinfo/Africa/Nouakchott
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Nouakchott differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Ouagadougou b/telegramer/include/pytz/zoneinfo/Africa/Ouagadougou
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Ouagadougou differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Porto-Novo b/telegramer/include/pytz/zoneinfo/Africa/Porto-Novo
new file mode 100644
index 0000000..afb6a4a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Porto-Novo differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Sao_Tome b/telegramer/include/pytz/zoneinfo/Africa/Sao_Tome
new file mode 100644
index 0000000..59f3759
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Sao_Tome differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Timbuktu b/telegramer/include/pytz/zoneinfo/Africa/Timbuktu
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Timbuktu differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Tripoli b/telegramer/include/pytz/zoneinfo/Africa/Tripoli
new file mode 100644
index 0000000..07b393b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Tripoli differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Tunis b/telegramer/include/pytz/zoneinfo/Africa/Tunis
new file mode 100644
index 0000000..427fa56
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Tunis differ
diff --git a/telegramer/include/pytz/zoneinfo/Africa/Windhoek b/telegramer/include/pytz/zoneinfo/Africa/Windhoek
new file mode 100644
index 0000000..abecd13
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Africa/Windhoek differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Adak b/telegramer/include/pytz/zoneinfo/America/Adak
new file mode 100644
index 0000000..4323649
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Adak differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Anchorage b/telegramer/include/pytz/zoneinfo/America/Anchorage
new file mode 100644
index 0000000..9bbb2fd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Anchorage differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Anguilla b/telegramer/include/pytz/zoneinfo/America/Anguilla
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Anguilla differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Antigua b/telegramer/include/pytz/zoneinfo/America/Antigua
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Antigua differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Araguaina b/telegramer/include/pytz/zoneinfo/America/Araguaina
new file mode 100644
index 0000000..49381b4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Araguaina differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Buenos_Aires b/telegramer/include/pytz/zoneinfo/America/Argentina/Buenos_Aires
new file mode 100644
index 0000000..260f86a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Buenos_Aires differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Catamarca b/telegramer/include/pytz/zoneinfo/America/Argentina/Catamarca
new file mode 100644
index 0000000..0ae222a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Catamarca differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/ComodRivadavia b/telegramer/include/pytz/zoneinfo/America/Argentina/ComodRivadavia
new file mode 100644
index 0000000..0ae222a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/ComodRivadavia differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Cordoba b/telegramer/include/pytz/zoneinfo/America/Argentina/Cordoba
new file mode 100644
index 0000000..da4c23a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Cordoba differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Jujuy b/telegramer/include/pytz/zoneinfo/America/Argentina/Jujuy
new file mode 100644
index 0000000..604b856
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Jujuy differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/La_Rioja b/telegramer/include/pytz/zoneinfo/America/Argentina/La_Rioja
new file mode 100644
index 0000000..2218e36
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/La_Rioja differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Mendoza b/telegramer/include/pytz/zoneinfo/America/Argentina/Mendoza
new file mode 100644
index 0000000..f9e677f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Mendoza differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Rio_Gallegos b/telegramer/include/pytz/zoneinfo/America/Argentina/Rio_Gallegos
new file mode 100644
index 0000000..c36587e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Rio_Gallegos differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Salta b/telegramer/include/pytz/zoneinfo/America/Argentina/Salta
new file mode 100644
index 0000000..0e797f2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Salta differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/San_Juan b/telegramer/include/pytz/zoneinfo/America/Argentina/San_Juan
new file mode 100644
index 0000000..2698495
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/San_Juan differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/San_Luis b/telegramer/include/pytz/zoneinfo/America/Argentina/San_Luis
new file mode 100644
index 0000000..fe50f62
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/San_Luis differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Tucuman b/telegramer/include/pytz/zoneinfo/America/Argentina/Tucuman
new file mode 100644
index 0000000..c954000
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Tucuman differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Argentina/Ushuaia b/telegramer/include/pytz/zoneinfo/America/Argentina/Ushuaia
new file mode 100644
index 0000000..3643628
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Argentina/Ushuaia differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Aruba b/telegramer/include/pytz/zoneinfo/America/Aruba
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Aruba differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Asuncion b/telegramer/include/pytz/zoneinfo/America/Asuncion
new file mode 100644
index 0000000..2f3bbda
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Asuncion differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Atikokan b/telegramer/include/pytz/zoneinfo/America/Atikokan
new file mode 100644
index 0000000..9964b9a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Atikokan differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Atka b/telegramer/include/pytz/zoneinfo/America/Atka
new file mode 100644
index 0000000..4323649
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Atka differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Bahia b/telegramer/include/pytz/zoneinfo/America/Bahia
new file mode 100644
index 0000000..15808d3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Bahia differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Bahia_Banderas b/telegramer/include/pytz/zoneinfo/America/Bahia_Banderas
new file mode 100644
index 0000000..896af3f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Bahia_Banderas differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Barbados b/telegramer/include/pytz/zoneinfo/America/Barbados
new file mode 100644
index 0000000..00cd045
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Barbados differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Belem b/telegramer/include/pytz/zoneinfo/America/Belem
new file mode 100644
index 0000000..60b5924
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Belem differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Belize b/telegramer/include/pytz/zoneinfo/America/Belize
new file mode 100644
index 0000000..e6f5dfa
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Belize differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Blanc-Sablon b/telegramer/include/pytz/zoneinfo/America/Blanc-Sablon
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Blanc-Sablon differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Boa_Vista b/telegramer/include/pytz/zoneinfo/America/Boa_Vista
new file mode 100644
index 0000000..978c331
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Boa_Vista differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Bogota b/telegramer/include/pytz/zoneinfo/America/Bogota
new file mode 100644
index 0000000..b2647d7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Bogota differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Boise b/telegramer/include/pytz/zoneinfo/America/Boise
new file mode 100644
index 0000000..f8d54e2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Boise differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Buenos_Aires b/telegramer/include/pytz/zoneinfo/America/Buenos_Aires
new file mode 100644
index 0000000..260f86a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Buenos_Aires differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Cambridge_Bay b/telegramer/include/pytz/zoneinfo/America/Cambridge_Bay
new file mode 100644
index 0000000..f8db4b6
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cambridge_Bay differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Campo_Grande b/telegramer/include/pytz/zoneinfo/America/Campo_Grande
new file mode 100644
index 0000000..8120624
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Campo_Grande differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Cancun b/telegramer/include/pytz/zoneinfo/America/Cancun
new file mode 100644
index 0000000..f907f0a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cancun differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Caracas b/telegramer/include/pytz/zoneinfo/America/Caracas
new file mode 100644
index 0000000..eedf725
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Caracas differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Catamarca b/telegramer/include/pytz/zoneinfo/America/Catamarca
new file mode 100644
index 0000000..0ae222a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Catamarca differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Cayenne b/telegramer/include/pytz/zoneinfo/America/Cayenne
new file mode 100644
index 0000000..e5bc06f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cayenne differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Cayman b/telegramer/include/pytz/zoneinfo/America/Cayman
new file mode 100644
index 0000000..9964b9a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cayman differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Chicago b/telegramer/include/pytz/zoneinfo/America/Chicago
new file mode 100644
index 0000000..a5b1617
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Chicago differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Chihuahua b/telegramer/include/pytz/zoneinfo/America/Chihuahua
new file mode 100644
index 0000000..8ed5f93
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Chihuahua differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Coral_Harbour b/telegramer/include/pytz/zoneinfo/America/Coral_Harbour
new file mode 100644
index 0000000..9964b9a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Coral_Harbour differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Cordoba b/telegramer/include/pytz/zoneinfo/America/Cordoba
new file mode 100644
index 0000000..da4c23a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cordoba differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Costa_Rica b/telegramer/include/pytz/zoneinfo/America/Costa_Rica
new file mode 100644
index 0000000..37cb85e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Costa_Rica differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Creston b/telegramer/include/pytz/zoneinfo/America/Creston
new file mode 100644
index 0000000..ac6bb0c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Creston differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Cuiaba b/telegramer/include/pytz/zoneinfo/America/Cuiaba
new file mode 100644
index 0000000..9bea3d4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Cuiaba differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Curacao b/telegramer/include/pytz/zoneinfo/America/Curacao
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Curacao differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Danmarkshavn b/telegramer/include/pytz/zoneinfo/America/Danmarkshavn
new file mode 100644
index 0000000..9549adc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Danmarkshavn differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Dawson b/telegramer/include/pytz/zoneinfo/America/Dawson
new file mode 100644
index 0000000..343b632
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Dawson differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Dawson_Creek b/telegramer/include/pytz/zoneinfo/America/Dawson_Creek
new file mode 100644
index 0000000..db9e339
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Dawson_Creek differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Denver b/telegramer/include/pytz/zoneinfo/America/Denver
new file mode 100644
index 0000000..5fbe26b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Denver differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Detroit b/telegramer/include/pytz/zoneinfo/America/Detroit
new file mode 100644
index 0000000..e104faa
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Detroit differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Dominica b/telegramer/include/pytz/zoneinfo/America/Dominica
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Dominica differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Edmonton b/telegramer/include/pytz/zoneinfo/America/Edmonton
new file mode 100644
index 0000000..cd78a6f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Edmonton differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Eirunepe b/telegramer/include/pytz/zoneinfo/America/Eirunepe
new file mode 100644
index 0000000..39d6dae
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Eirunepe differ
diff --git a/telegramer/include/pytz/zoneinfo/America/El_Salvador b/telegramer/include/pytz/zoneinfo/America/El_Salvador
new file mode 100644
index 0000000..e2f2230
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/El_Salvador differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Ensenada b/telegramer/include/pytz/zoneinfo/America/Ensenada
new file mode 100644
index 0000000..ada6bf7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Ensenada differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Fort_Nelson b/telegramer/include/pytz/zoneinfo/America/Fort_Nelson
new file mode 100644
index 0000000..5a0b7f1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Fort_Nelson differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Fort_Wayne b/telegramer/include/pytz/zoneinfo/America/Fort_Wayne
new file mode 100644
index 0000000..09511cc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Fort_Wayne differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Fortaleza b/telegramer/include/pytz/zoneinfo/America/Fortaleza
new file mode 100644
index 0000000..be57dc2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Fortaleza differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Glace_Bay b/telegramer/include/pytz/zoneinfo/America/Glace_Bay
new file mode 100644
index 0000000..48412a4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Glace_Bay differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Godthab b/telegramer/include/pytz/zoneinfo/America/Godthab
new file mode 100644
index 0000000..0160308
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Godthab differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Goose_Bay b/telegramer/include/pytz/zoneinfo/America/Goose_Bay
new file mode 100644
index 0000000..a3f2990
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Goose_Bay differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Grand_Turk b/telegramer/include/pytz/zoneinfo/America/Grand_Turk
new file mode 100644
index 0000000..06da1a6
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Grand_Turk differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Grenada b/telegramer/include/pytz/zoneinfo/America/Grenada
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Grenada differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Guadeloupe b/telegramer/include/pytz/zoneinfo/America/Guadeloupe
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Guadeloupe differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Guatemala b/telegramer/include/pytz/zoneinfo/America/Guatemala
new file mode 100644
index 0000000..407138c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Guatemala differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Guayaquil b/telegramer/include/pytz/zoneinfo/America/Guayaquil
new file mode 100644
index 0000000..0559a7a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Guayaquil differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Guyana b/telegramer/include/pytz/zoneinfo/America/Guyana
new file mode 100644
index 0000000..7af58e5
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Guyana differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Halifax b/telegramer/include/pytz/zoneinfo/America/Halifax
new file mode 100644
index 0000000..756099a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Halifax differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Havana b/telegramer/include/pytz/zoneinfo/America/Havana
new file mode 100644
index 0000000..b69ac45
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Havana differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Hermosillo b/telegramer/include/pytz/zoneinfo/America/Hermosillo
new file mode 100644
index 0000000..791a9fa
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Hermosillo differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Indianapolis b/telegramer/include/pytz/zoneinfo/America/Indiana/Indianapolis
new file mode 100644
index 0000000..09511cc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Indianapolis differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Knox b/telegramer/include/pytz/zoneinfo/America/Indiana/Knox
new file mode 100644
index 0000000..fcd408d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Knox differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Marengo b/telegramer/include/pytz/zoneinfo/America/Indiana/Marengo
new file mode 100644
index 0000000..1abf75e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Marengo differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Petersburg b/telegramer/include/pytz/zoneinfo/America/Indiana/Petersburg
new file mode 100644
index 0000000..0133548
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Petersburg differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Tell_City b/telegramer/include/pytz/zoneinfo/America/Indiana/Tell_City
new file mode 100644
index 0000000..7bbb653
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Tell_City differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Vevay b/telegramer/include/pytz/zoneinfo/America/Indiana/Vevay
new file mode 100644
index 0000000..d236b7c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Vevay differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Vincennes b/telegramer/include/pytz/zoneinfo/America/Indiana/Vincennes
new file mode 100644
index 0000000..c818929
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Vincennes differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Indiana/Winamac b/telegramer/include/pytz/zoneinfo/America/Indiana/Winamac
new file mode 100644
index 0000000..630935c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indiana/Winamac differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Indianapolis b/telegramer/include/pytz/zoneinfo/America/Indianapolis
new file mode 100644
index 0000000..09511cc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Indianapolis differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Inuvik b/telegramer/include/pytz/zoneinfo/America/Inuvik
new file mode 100644
index 0000000..87bb355
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Inuvik differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Iqaluit b/telegramer/include/pytz/zoneinfo/America/Iqaluit
new file mode 100644
index 0000000..c8138bd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Iqaluit differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Jamaica b/telegramer/include/pytz/zoneinfo/America/Jamaica
new file mode 100644
index 0000000..2a9b7fd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Jamaica differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Jujuy b/telegramer/include/pytz/zoneinfo/America/Jujuy
new file mode 100644
index 0000000..604b856
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Jujuy differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Juneau b/telegramer/include/pytz/zoneinfo/America/Juneau
new file mode 100644
index 0000000..451f349
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Juneau differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Kentucky/Louisville b/telegramer/include/pytz/zoneinfo/America/Kentucky/Louisville
new file mode 100644
index 0000000..177836e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Kentucky/Louisville differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Kentucky/Monticello b/telegramer/include/pytz/zoneinfo/America/Kentucky/Monticello
new file mode 100644
index 0000000..438e3ea
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Kentucky/Monticello differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Knox_IN b/telegramer/include/pytz/zoneinfo/America/Knox_IN
new file mode 100644
index 0000000..fcd408d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Knox_IN differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Kralendijk b/telegramer/include/pytz/zoneinfo/America/Kralendijk
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Kralendijk differ
diff --git a/telegramer/include/pytz/zoneinfo/America/La_Paz b/telegramer/include/pytz/zoneinfo/America/La_Paz
new file mode 100644
index 0000000..a101372
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/La_Paz differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Lima b/telegramer/include/pytz/zoneinfo/America/Lima
new file mode 100644
index 0000000..3c6529b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Lima differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Los_Angeles b/telegramer/include/pytz/zoneinfo/America/Los_Angeles
new file mode 100644
index 0000000..9dad4f4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Los_Angeles differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Louisville b/telegramer/include/pytz/zoneinfo/America/Louisville
new file mode 100644
index 0000000..177836e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Louisville differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Lower_Princes b/telegramer/include/pytz/zoneinfo/America/Lower_Princes
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Lower_Princes differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Maceio b/telegramer/include/pytz/zoneinfo/America/Maceio
new file mode 100644
index 0000000..bc8b951
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Maceio differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Managua b/telegramer/include/pytz/zoneinfo/America/Managua
new file mode 100644
index 0000000..e0242bf
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Managua differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Manaus b/telegramer/include/pytz/zoneinfo/America/Manaus
new file mode 100644
index 0000000..63d58f8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Manaus differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Marigot b/telegramer/include/pytz/zoneinfo/America/Marigot
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Marigot differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Martinique b/telegramer/include/pytz/zoneinfo/America/Martinique
new file mode 100644
index 0000000..8df43dc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Martinique differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Matamoros b/telegramer/include/pytz/zoneinfo/America/Matamoros
new file mode 100644
index 0000000..047968d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Matamoros differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Mazatlan b/telegramer/include/pytz/zoneinfo/America/Mazatlan
new file mode 100644
index 0000000..e4a7857
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Mazatlan differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Mendoza b/telegramer/include/pytz/zoneinfo/America/Mendoza
new file mode 100644
index 0000000..f9e677f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Mendoza differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Menominee b/telegramer/include/pytz/zoneinfo/America/Menominee
new file mode 100644
index 0000000..3146138
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Menominee differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Merida b/telegramer/include/pytz/zoneinfo/America/Merida
new file mode 100644
index 0000000..ea852da
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Merida differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Metlakatla b/telegramer/include/pytz/zoneinfo/America/Metlakatla
new file mode 100644
index 0000000..1e94be3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Metlakatla differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Mexico_City b/telegramer/include/pytz/zoneinfo/America/Mexico_City
new file mode 100644
index 0000000..e7fb6f2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Mexico_City differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Miquelon b/telegramer/include/pytz/zoneinfo/America/Miquelon
new file mode 100644
index 0000000..b924b71
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Miquelon differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Moncton b/telegramer/include/pytz/zoneinfo/America/Moncton
new file mode 100644
index 0000000..9df8d0f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Moncton differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Monterrey b/telegramer/include/pytz/zoneinfo/America/Monterrey
new file mode 100644
index 0000000..a8928c8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Monterrey differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Montevideo b/telegramer/include/pytz/zoneinfo/America/Montevideo
new file mode 100644
index 0000000..2f357bc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Montevideo differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Montreal b/telegramer/include/pytz/zoneinfo/America/Montreal
new file mode 100644
index 0000000..6752c5b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Montreal differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Montserrat b/telegramer/include/pytz/zoneinfo/America/Montserrat
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Montserrat differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Nassau b/telegramer/include/pytz/zoneinfo/America/Nassau
new file mode 100644
index 0000000..6752c5b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Nassau differ
diff --git a/telegramer/include/pytz/zoneinfo/America/New_York b/telegramer/include/pytz/zoneinfo/America/New_York
new file mode 100644
index 0000000..2f75480
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/New_York differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Nipigon b/telegramer/include/pytz/zoneinfo/America/Nipigon
new file mode 100644
index 0000000..f6a856e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Nipigon differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Nome b/telegramer/include/pytz/zoneinfo/America/Nome
new file mode 100644
index 0000000..10998df
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Nome differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Noronha b/telegramer/include/pytz/zoneinfo/America/Noronha
new file mode 100644
index 0000000..f140726
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Noronha differ
diff --git a/telegramer/include/pytz/zoneinfo/America/North_Dakota/Beulah b/telegramer/include/pytz/zoneinfo/America/North_Dakota/Beulah
new file mode 100644
index 0000000..246345d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/North_Dakota/Beulah differ
diff --git a/telegramer/include/pytz/zoneinfo/America/North_Dakota/Center b/telegramer/include/pytz/zoneinfo/America/North_Dakota/Center
new file mode 100644
index 0000000..1fa0703
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/North_Dakota/Center differ
diff --git a/telegramer/include/pytz/zoneinfo/America/North_Dakota/New_Salem b/telegramer/include/pytz/zoneinfo/America/North_Dakota/New_Salem
new file mode 100644
index 0000000..123f2ae
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/North_Dakota/New_Salem differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Nuuk b/telegramer/include/pytz/zoneinfo/America/Nuuk
new file mode 100644
index 0000000..0160308
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Nuuk differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Ojinaga b/telegramer/include/pytz/zoneinfo/America/Ojinaga
new file mode 100644
index 0000000..fc4a03e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Ojinaga differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Panama b/telegramer/include/pytz/zoneinfo/America/Panama
new file mode 100644
index 0000000..9964b9a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Panama differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Pangnirtung b/telegramer/include/pytz/zoneinfo/America/Pangnirtung
new file mode 100644
index 0000000..3e4e0db
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Pangnirtung differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Paramaribo b/telegramer/include/pytz/zoneinfo/America/Paramaribo
new file mode 100644
index 0000000..bc8a6ed
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Paramaribo differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Phoenix b/telegramer/include/pytz/zoneinfo/America/Phoenix
new file mode 100644
index 0000000..ac6bb0c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Phoenix differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Port-au-Prince b/telegramer/include/pytz/zoneinfo/America/Port-au-Prince
new file mode 100644
index 0000000..287f143
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Port-au-Prince differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Port_of_Spain b/telegramer/include/pytz/zoneinfo/America/Port_of_Spain
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Port_of_Spain differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Porto_Acre b/telegramer/include/pytz/zoneinfo/America/Porto_Acre
new file mode 100644
index 0000000..a374cb4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Porto_Acre differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Porto_Velho b/telegramer/include/pytz/zoneinfo/America/Porto_Velho
new file mode 100644
index 0000000..2e873a5
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Porto_Velho differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Puerto_Rico b/telegramer/include/pytz/zoneinfo/America/Puerto_Rico
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Puerto_Rico differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Punta_Arenas b/telegramer/include/pytz/zoneinfo/America/Punta_Arenas
new file mode 100644
index 0000000..13bd1d9
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Punta_Arenas differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Rainy_River b/telegramer/include/pytz/zoneinfo/America/Rainy_River
new file mode 100644
index 0000000..ea66099
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Rainy_River differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Rankin_Inlet b/telegramer/include/pytz/zoneinfo/America/Rankin_Inlet
new file mode 100644
index 0000000..3a70587
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Rankin_Inlet differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Recife b/telegramer/include/pytz/zoneinfo/America/Recife
new file mode 100644
index 0000000..d7abb16
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Recife differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Regina b/telegramer/include/pytz/zoneinfo/America/Regina
new file mode 100644
index 0000000..20c9c84
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Regina differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Resolute b/telegramer/include/pytz/zoneinfo/America/Resolute
new file mode 100644
index 0000000..0a73b75
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Resolute differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Rio_Branco b/telegramer/include/pytz/zoneinfo/America/Rio_Branco
new file mode 100644
index 0000000..a374cb4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Rio_Branco differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Rosario b/telegramer/include/pytz/zoneinfo/America/Rosario
new file mode 100644
index 0000000..da4c23a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Rosario differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Santa_Isabel b/telegramer/include/pytz/zoneinfo/America/Santa_Isabel
new file mode 100644
index 0000000..ada6bf7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Santa_Isabel differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Santarem b/telegramer/include/pytz/zoneinfo/America/Santarem
new file mode 100644
index 0000000..c28f360
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Santarem differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Santiago b/telegramer/include/pytz/zoneinfo/America/Santiago
new file mode 100644
index 0000000..aa29060
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Santiago differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Santo_Domingo b/telegramer/include/pytz/zoneinfo/America/Santo_Domingo
new file mode 100644
index 0000000..4fe36fd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Santo_Domingo differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Sao_Paulo b/telegramer/include/pytz/zoneinfo/America/Sao_Paulo
new file mode 100644
index 0000000..13ff083
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Sao_Paulo differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Scoresbysund b/telegramer/include/pytz/zoneinfo/America/Scoresbysund
new file mode 100644
index 0000000..e20e9e1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Scoresbysund differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Shiprock b/telegramer/include/pytz/zoneinfo/America/Shiprock
new file mode 100644
index 0000000..5fbe26b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Shiprock differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Sitka b/telegramer/include/pytz/zoneinfo/America/Sitka
new file mode 100644
index 0000000..31f7061
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Sitka differ
diff --git a/telegramer/include/pytz/zoneinfo/America/St_Barthelemy b/telegramer/include/pytz/zoneinfo/America/St_Barthelemy
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Barthelemy differ
diff --git a/telegramer/include/pytz/zoneinfo/America/St_Johns b/telegramer/include/pytz/zoneinfo/America/St_Johns
new file mode 100644
index 0000000..65a5b0c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Johns differ
diff --git a/telegramer/include/pytz/zoneinfo/America/St_Kitts b/telegramer/include/pytz/zoneinfo/America/St_Kitts
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Kitts differ
diff --git a/telegramer/include/pytz/zoneinfo/America/St_Lucia b/telegramer/include/pytz/zoneinfo/America/St_Lucia
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Lucia differ
diff --git a/telegramer/include/pytz/zoneinfo/America/St_Thomas b/telegramer/include/pytz/zoneinfo/America/St_Thomas
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Thomas differ
diff --git a/telegramer/include/pytz/zoneinfo/America/St_Vincent b/telegramer/include/pytz/zoneinfo/America/St_Vincent
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/St_Vincent differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Swift_Current b/telegramer/include/pytz/zoneinfo/America/Swift_Current
new file mode 100644
index 0000000..8e9ef25
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Swift_Current differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Tegucigalpa b/telegramer/include/pytz/zoneinfo/America/Tegucigalpa
new file mode 100644
index 0000000..2adacb2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Tegucigalpa differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Thule b/telegramer/include/pytz/zoneinfo/America/Thule
new file mode 100644
index 0000000..6f802f1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Thule differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Thunder_Bay b/telegramer/include/pytz/zoneinfo/America/Thunder_Bay
new file mode 100644
index 0000000..e504c9a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Thunder_Bay differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Tijuana b/telegramer/include/pytz/zoneinfo/America/Tijuana
new file mode 100644
index 0000000..ada6bf7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Tijuana differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Toronto b/telegramer/include/pytz/zoneinfo/America/Toronto
new file mode 100644
index 0000000..6752c5b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Toronto differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Tortola b/telegramer/include/pytz/zoneinfo/America/Tortola
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Tortola differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Vancouver b/telegramer/include/pytz/zoneinfo/America/Vancouver
new file mode 100644
index 0000000..bb60cbc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Vancouver differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Virgin b/telegramer/include/pytz/zoneinfo/America/Virgin
new file mode 100644
index 0000000..a662a57
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Virgin differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Whitehorse b/telegramer/include/pytz/zoneinfo/America/Whitehorse
new file mode 100644
index 0000000..9ee229c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Whitehorse differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Winnipeg b/telegramer/include/pytz/zoneinfo/America/Winnipeg
new file mode 100644
index 0000000..ac40299
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Winnipeg differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Yakutat b/telegramer/include/pytz/zoneinfo/America/Yakutat
new file mode 100644
index 0000000..da209f9
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Yakutat differ
diff --git a/telegramer/include/pytz/zoneinfo/America/Yellowknife b/telegramer/include/pytz/zoneinfo/America/Yellowknife
new file mode 100644
index 0000000..e6afa39
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/America/Yellowknife differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Casey b/telegramer/include/pytz/zoneinfo/Antarctica/Casey
new file mode 100644
index 0000000..cbcbe4e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Casey differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Davis b/telegramer/include/pytz/zoneinfo/Antarctica/Davis
new file mode 100644
index 0000000..916f2c2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Davis differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/DumontDUrville b/telegramer/include/pytz/zoneinfo/Antarctica/DumontDUrville
new file mode 100644
index 0000000..920ad27
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/DumontDUrville differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Macquarie b/telegramer/include/pytz/zoneinfo/Antarctica/Macquarie
new file mode 100644
index 0000000..9e7cc68
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Macquarie differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Mawson b/telegramer/include/pytz/zoneinfo/Antarctica/Mawson
new file mode 100644
index 0000000..b32e7fd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Mawson differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/McMurdo b/telegramer/include/pytz/zoneinfo/Antarctica/McMurdo
new file mode 100644
index 0000000..6575fdc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/McMurdo differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Palmer b/telegramer/include/pytz/zoneinfo/Antarctica/Palmer
new file mode 100644
index 0000000..3dd85f8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Palmer differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Rothera b/telegramer/include/pytz/zoneinfo/Antarctica/Rothera
new file mode 100644
index 0000000..8b2430a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Rothera differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/South_Pole b/telegramer/include/pytz/zoneinfo/Antarctica/South_Pole
new file mode 100644
index 0000000..6575fdc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/South_Pole differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Syowa b/telegramer/include/pytz/zoneinfo/Antarctica/Syowa
new file mode 100644
index 0000000..2aea25f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Syowa differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Troll b/telegramer/include/pytz/zoneinfo/Antarctica/Troll
new file mode 100644
index 0000000..5e565da
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Troll differ
diff --git a/telegramer/include/pytz/zoneinfo/Antarctica/Vostok b/telegramer/include/pytz/zoneinfo/Antarctica/Vostok
new file mode 100644
index 0000000..7283053
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Antarctica/Vostok differ
diff --git a/telegramer/include/pytz/zoneinfo/Arctic/Longyearbyen b/telegramer/include/pytz/zoneinfo/Arctic/Longyearbyen
new file mode 100644
index 0000000..15a34c3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Arctic/Longyearbyen differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Aden b/telegramer/include/pytz/zoneinfo/Asia/Aden
new file mode 100644
index 0000000..2aea25f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Aden differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Almaty b/telegramer/include/pytz/zoneinfo/Asia/Almaty
new file mode 100644
index 0000000..a4b0077
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Almaty differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Amman b/telegramer/include/pytz/zoneinfo/Asia/Amman
new file mode 100644
index 0000000..5dcf7e0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Amman differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Anadyr b/telegramer/include/pytz/zoneinfo/Asia/Anadyr
new file mode 100644
index 0000000..6ed8b7c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Anadyr differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Aqtau b/telegramer/include/pytz/zoneinfo/Asia/Aqtau
new file mode 100644
index 0000000..e2d0f91
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Aqtau differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Aqtobe b/telegramer/include/pytz/zoneinfo/Asia/Aqtobe
new file mode 100644
index 0000000..06f0a13
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Aqtobe differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ashgabat b/telegramer/include/pytz/zoneinfo/Asia/Ashgabat
new file mode 100644
index 0000000..73891af
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ashgabat differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ashkhabad b/telegramer/include/pytz/zoneinfo/Asia/Ashkhabad
new file mode 100644
index 0000000..73891af
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ashkhabad differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Atyrau b/telegramer/include/pytz/zoneinfo/Asia/Atyrau
new file mode 100644
index 0000000..8b5153e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Atyrau differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Baghdad b/telegramer/include/pytz/zoneinfo/Asia/Baghdad
new file mode 100644
index 0000000..f7162ed
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Baghdad differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Bahrain b/telegramer/include/pytz/zoneinfo/Asia/Bahrain
new file mode 100644
index 0000000..63188b2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Bahrain differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Baku b/telegramer/include/pytz/zoneinfo/Asia/Baku
new file mode 100644
index 0000000..a0de74b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Baku differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Bangkok b/telegramer/include/pytz/zoneinfo/Asia/Bangkok
new file mode 100644
index 0000000..c292ac5
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Bangkok differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Barnaul b/telegramer/include/pytz/zoneinfo/Asia/Barnaul
new file mode 100644
index 0000000..759592a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Barnaul differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Beirut b/telegramer/include/pytz/zoneinfo/Asia/Beirut
new file mode 100644
index 0000000..fb266ed
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Beirut differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Bishkek b/telegramer/include/pytz/zoneinfo/Asia/Bishkek
new file mode 100644
index 0000000..f6e20dd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Bishkek differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Brunei b/telegramer/include/pytz/zoneinfo/Asia/Brunei
new file mode 100644
index 0000000..3dab0ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Brunei differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Calcutta b/telegramer/include/pytz/zoneinfo/Asia/Calcutta
new file mode 100644
index 0000000..0014046
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Calcutta differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Chita b/telegramer/include/pytz/zoneinfo/Asia/Chita
new file mode 100644
index 0000000..c4149c0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Chita differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Choibalsan b/telegramer/include/pytz/zoneinfo/Asia/Choibalsan
new file mode 100644
index 0000000..e48daa8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Choibalsan differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Chongqing b/telegramer/include/pytz/zoneinfo/Asia/Chongqing
new file mode 100644
index 0000000..91f6f8b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Chongqing differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Chungking b/telegramer/include/pytz/zoneinfo/Asia/Chungking
new file mode 100644
index 0000000..91f6f8b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Chungking differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Colombo b/telegramer/include/pytz/zoneinfo/Asia/Colombo
new file mode 100644
index 0000000..62c64d8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Colombo differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dacca b/telegramer/include/pytz/zoneinfo/Asia/Dacca
new file mode 100644
index 0000000..b11c928
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dacca differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Damascus b/telegramer/include/pytz/zoneinfo/Asia/Damascus
new file mode 100644
index 0000000..d9104a7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Damascus differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dhaka b/telegramer/include/pytz/zoneinfo/Asia/Dhaka
new file mode 100644
index 0000000..b11c928
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dhaka differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dili b/telegramer/include/pytz/zoneinfo/Asia/Dili
new file mode 100644
index 0000000..30943bb
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dili differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dubai b/telegramer/include/pytz/zoneinfo/Asia/Dubai
new file mode 100644
index 0000000..fc0a589
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dubai differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Dushanbe b/telegramer/include/pytz/zoneinfo/Asia/Dushanbe
new file mode 100644
index 0000000..82d85b8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Dushanbe differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Famagusta b/telegramer/include/pytz/zoneinfo/Asia/Famagusta
new file mode 100644
index 0000000..653b146
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Famagusta differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Gaza b/telegramer/include/pytz/zoneinfo/Asia/Gaza
new file mode 100644
index 0000000..266981a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Gaza differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Harbin b/telegramer/include/pytz/zoneinfo/Asia/Harbin
new file mode 100644
index 0000000..91f6f8b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Harbin differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Hebron b/telegramer/include/pytz/zoneinfo/Asia/Hebron
new file mode 100644
index 0000000..0078bf0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Hebron differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ho_Chi_Minh b/telegramer/include/pytz/zoneinfo/Asia/Ho_Chi_Minh
new file mode 100644
index 0000000..e2934e3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ho_Chi_Minh differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Hong_Kong b/telegramer/include/pytz/zoneinfo/Asia/Hong_Kong
new file mode 100644
index 0000000..23d0375
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Hong_Kong differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Hovd b/telegramer/include/pytz/zoneinfo/Asia/Hovd
new file mode 100644
index 0000000..4cb800a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Hovd differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Irkutsk b/telegramer/include/pytz/zoneinfo/Asia/Irkutsk
new file mode 100644
index 0000000..4dcbbb7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Irkutsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Istanbul b/telegramer/include/pytz/zoneinfo/Asia/Istanbul
new file mode 100644
index 0000000..508446b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Istanbul differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Jakarta b/telegramer/include/pytz/zoneinfo/Asia/Jakarta
new file mode 100644
index 0000000..5baa3a8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Jakarta differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Jayapura b/telegramer/include/pytz/zoneinfo/Asia/Jayapura
new file mode 100644
index 0000000..3002c82
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Jayapura differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Jerusalem b/telegramer/include/pytz/zoneinfo/Asia/Jerusalem
new file mode 100644
index 0000000..1ebd066
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Jerusalem differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kabul b/telegramer/include/pytz/zoneinfo/Asia/Kabul
new file mode 100644
index 0000000..d19b9bd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kabul differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kamchatka b/telegramer/include/pytz/zoneinfo/Asia/Kamchatka
new file mode 100644
index 0000000..3e80b4e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kamchatka differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Karachi b/telegramer/include/pytz/zoneinfo/Asia/Karachi
new file mode 100644
index 0000000..ba65c0e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Karachi differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kashgar b/telegramer/include/pytz/zoneinfo/Asia/Kashgar
new file mode 100644
index 0000000..faa14d9
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kashgar differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kathmandu b/telegramer/include/pytz/zoneinfo/Asia/Kathmandu
new file mode 100644
index 0000000..a5d5107
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kathmandu differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Katmandu b/telegramer/include/pytz/zoneinfo/Asia/Katmandu
new file mode 100644
index 0000000..a5d5107
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Katmandu differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Khandyga b/telegramer/include/pytz/zoneinfo/Asia/Khandyga
new file mode 100644
index 0000000..72bea64
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Khandyga differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kolkata b/telegramer/include/pytz/zoneinfo/Asia/Kolkata
new file mode 100644
index 0000000..0014046
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kolkata differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Krasnoyarsk b/telegramer/include/pytz/zoneinfo/Asia/Krasnoyarsk
new file mode 100644
index 0000000..30c6f16
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Krasnoyarsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kuala_Lumpur b/telegramer/include/pytz/zoneinfo/Asia/Kuala_Lumpur
new file mode 100644
index 0000000..612b01e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kuala_Lumpur differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kuching b/telegramer/include/pytz/zoneinfo/Asia/Kuching
new file mode 100644
index 0000000..c86750c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kuching differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Kuwait b/telegramer/include/pytz/zoneinfo/Asia/Kuwait
new file mode 100644
index 0000000..2aea25f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Kuwait differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Macao b/telegramer/include/pytz/zoneinfo/Asia/Macao
new file mode 100644
index 0000000..cac6506
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Macao differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Macau b/telegramer/include/pytz/zoneinfo/Asia/Macau
new file mode 100644
index 0000000..cac6506
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Macau differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Magadan b/telegramer/include/pytz/zoneinfo/Asia/Magadan
new file mode 100644
index 0000000..b4fcac1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Magadan differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Makassar b/telegramer/include/pytz/zoneinfo/Asia/Makassar
new file mode 100644
index 0000000..556ba86
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Makassar differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Manila b/telegramer/include/pytz/zoneinfo/Asia/Manila
new file mode 100644
index 0000000..f4f4b04
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Manila differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Muscat b/telegramer/include/pytz/zoneinfo/Asia/Muscat
new file mode 100644
index 0000000..fc0a589
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Muscat differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Nicosia b/telegramer/include/pytz/zoneinfo/Asia/Nicosia
new file mode 100644
index 0000000..f7f10ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Nicosia differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Novokuznetsk b/telegramer/include/pytz/zoneinfo/Asia/Novokuznetsk
new file mode 100644
index 0000000..d983276
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Novokuznetsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Novosibirsk b/telegramer/include/pytz/zoneinfo/Asia/Novosibirsk
new file mode 100644
index 0000000..e0ee5fc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Novosibirsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Omsk b/telegramer/include/pytz/zoneinfo/Asia/Omsk
new file mode 100644
index 0000000..b29b769
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Omsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Oral b/telegramer/include/pytz/zoneinfo/Asia/Oral
new file mode 100644
index 0000000..ad1f9ca
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Oral differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Phnom_Penh b/telegramer/include/pytz/zoneinfo/Asia/Phnom_Penh
new file mode 100644
index 0000000..c292ac5
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Phnom_Penh differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Pontianak b/telegramer/include/pytz/zoneinfo/Asia/Pontianak
new file mode 100644
index 0000000..12ce24c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Pontianak differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Pyongyang b/telegramer/include/pytz/zoneinfo/Asia/Pyongyang
new file mode 100644
index 0000000..7ad7e0b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Pyongyang differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Qatar b/telegramer/include/pytz/zoneinfo/Asia/Qatar
new file mode 100644
index 0000000..63188b2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Qatar differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Qostanay b/telegramer/include/pytz/zoneinfo/Asia/Qostanay
new file mode 100644
index 0000000..73b9d96
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Qostanay differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Qyzylorda b/telegramer/include/pytz/zoneinfo/Asia/Qyzylorda
new file mode 100644
index 0000000..c2fe4c1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Qyzylorda differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Rangoon b/telegramer/include/pytz/zoneinfo/Asia/Rangoon
new file mode 100644
index 0000000..dd77395
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Rangoon differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Riyadh b/telegramer/include/pytz/zoneinfo/Asia/Riyadh
new file mode 100644
index 0000000..2aea25f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Riyadh differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Saigon b/telegramer/include/pytz/zoneinfo/Asia/Saigon
new file mode 100644
index 0000000..e2934e3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Saigon differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Sakhalin b/telegramer/include/pytz/zoneinfo/Asia/Sakhalin
new file mode 100644
index 0000000..485459c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Sakhalin differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Samarkand b/telegramer/include/pytz/zoneinfo/Asia/Samarkand
new file mode 100644
index 0000000..030d47c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Samarkand differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Seoul b/telegramer/include/pytz/zoneinfo/Asia/Seoul
new file mode 100644
index 0000000..96199e7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Seoul differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Shanghai b/telegramer/include/pytz/zoneinfo/Asia/Shanghai
new file mode 100644
index 0000000..91f6f8b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Shanghai differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Singapore b/telegramer/include/pytz/zoneinfo/Asia/Singapore
new file mode 100644
index 0000000..2364b21
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Singapore differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Srednekolymsk b/telegramer/include/pytz/zoneinfo/Asia/Srednekolymsk
new file mode 100644
index 0000000..261a983
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Srednekolymsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Taipei b/telegramer/include/pytz/zoneinfo/Asia/Taipei
new file mode 100644
index 0000000..24c4344
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Taipei differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tashkent b/telegramer/include/pytz/zoneinfo/Asia/Tashkent
new file mode 100644
index 0000000..32a9d7d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tashkent differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tbilisi b/telegramer/include/pytz/zoneinfo/Asia/Tbilisi
new file mode 100644
index 0000000..b608d79
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tbilisi differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tehran b/telegramer/include/pytz/zoneinfo/Asia/Tehran
new file mode 100644
index 0000000..8cec5ad
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tehran differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tel_Aviv b/telegramer/include/pytz/zoneinfo/Asia/Tel_Aviv
new file mode 100644
index 0000000..1ebd066
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tel_Aviv differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Thimbu b/telegramer/include/pytz/zoneinfo/Asia/Thimbu
new file mode 100644
index 0000000..fe409c7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Thimbu differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Thimphu b/telegramer/include/pytz/zoneinfo/Asia/Thimphu
new file mode 100644
index 0000000..fe409c7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Thimphu differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tokyo b/telegramer/include/pytz/zoneinfo/Asia/Tokyo
new file mode 100644
index 0000000..26f4d34
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tokyo differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Tomsk b/telegramer/include/pytz/zoneinfo/Asia/Tomsk
new file mode 100644
index 0000000..670e2ad
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Tomsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ujung_Pandang b/telegramer/include/pytz/zoneinfo/Asia/Ujung_Pandang
new file mode 100644
index 0000000..556ba86
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ujung_Pandang differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ulaanbaatar b/telegramer/include/pytz/zoneinfo/Asia/Ulaanbaatar
new file mode 100644
index 0000000..2e20cc3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ulaanbaatar differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ulan_Bator b/telegramer/include/pytz/zoneinfo/Asia/Ulan_Bator
new file mode 100644
index 0000000..2e20cc3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ulan_Bator differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Urumqi b/telegramer/include/pytz/zoneinfo/Asia/Urumqi
new file mode 100644
index 0000000..faa14d9
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Urumqi differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Ust-Nera b/telegramer/include/pytz/zoneinfo/Asia/Ust-Nera
new file mode 100644
index 0000000..9e4a78f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Ust-Nera differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Vientiane b/telegramer/include/pytz/zoneinfo/Asia/Vientiane
new file mode 100644
index 0000000..c292ac5
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Vientiane differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Vladivostok b/telegramer/include/pytz/zoneinfo/Asia/Vladivostok
new file mode 100644
index 0000000..8ab253c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Vladivostok differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Yakutsk b/telegramer/include/pytz/zoneinfo/Asia/Yakutsk
new file mode 100644
index 0000000..c815e99
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Yakutsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Yangon b/telegramer/include/pytz/zoneinfo/Asia/Yangon
new file mode 100644
index 0000000..dd77395
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Yangon differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Yekaterinburg b/telegramer/include/pytz/zoneinfo/Asia/Yekaterinburg
new file mode 100644
index 0000000..6958d7e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Yekaterinburg differ
diff --git a/telegramer/include/pytz/zoneinfo/Asia/Yerevan b/telegramer/include/pytz/zoneinfo/Asia/Yerevan
new file mode 100644
index 0000000..250bfe0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Asia/Yerevan differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Azores b/telegramer/include/pytz/zoneinfo/Atlantic/Azores
new file mode 100644
index 0000000..00a564f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Azores differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Bermuda b/telegramer/include/pytz/zoneinfo/Atlantic/Bermuda
new file mode 100644
index 0000000..527524e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Bermuda differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Canary b/telegramer/include/pytz/zoneinfo/Atlantic/Canary
new file mode 100644
index 0000000..f319215
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Canary differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Cape_Verde b/telegramer/include/pytz/zoneinfo/Atlantic/Cape_Verde
new file mode 100644
index 0000000..e2a49d2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Cape_Verde differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Faeroe b/telegramer/include/pytz/zoneinfo/Atlantic/Faeroe
new file mode 100644
index 0000000..4dab7ef
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Faeroe differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Faroe b/telegramer/include/pytz/zoneinfo/Atlantic/Faroe
new file mode 100644
index 0000000..4dab7ef
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Faroe differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Jan_Mayen b/telegramer/include/pytz/zoneinfo/Atlantic/Jan_Mayen
new file mode 100644
index 0000000..15a34c3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Jan_Mayen differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Madeira b/telegramer/include/pytz/zoneinfo/Atlantic/Madeira
new file mode 100644
index 0000000..7ddcd88
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Madeira differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Reykjavik b/telegramer/include/pytz/zoneinfo/Atlantic/Reykjavik
new file mode 100644
index 0000000..10e0fc8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Reykjavik differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/South_Georgia b/telegramer/include/pytz/zoneinfo/Atlantic/South_Georgia
new file mode 100644
index 0000000..4466608
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/South_Georgia differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/St_Helena b/telegramer/include/pytz/zoneinfo/Atlantic/St_Helena
new file mode 100644
index 0000000..28b32ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/St_Helena differ
diff --git a/telegramer/include/pytz/zoneinfo/Atlantic/Stanley b/telegramer/include/pytz/zoneinfo/Atlantic/Stanley
new file mode 100644
index 0000000..88077f1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Atlantic/Stanley differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/ACT b/telegramer/include/pytz/zoneinfo/Australia/ACT
new file mode 100644
index 0000000..0aea4c3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/ACT differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Adelaide b/telegramer/include/pytz/zoneinfo/Australia/Adelaide
new file mode 100644
index 0000000..f5dedca
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Adelaide differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Brisbane b/telegramer/include/pytz/zoneinfo/Australia/Brisbane
new file mode 100644
index 0000000..7ff9949
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Brisbane differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Broken_Hill b/telegramer/include/pytz/zoneinfo/Australia/Broken_Hill
new file mode 100644
index 0000000..698c76e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Broken_Hill differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Canberra b/telegramer/include/pytz/zoneinfo/Australia/Canberra
new file mode 100644
index 0000000..0aea4c3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Canberra differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Currie b/telegramer/include/pytz/zoneinfo/Australia/Currie
new file mode 100644
index 0000000..3adb8e1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Currie differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Darwin b/telegramer/include/pytz/zoneinfo/Australia/Darwin
new file mode 100644
index 0000000..74a3087
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Darwin differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Eucla b/telegramer/include/pytz/zoneinfo/Australia/Eucla
new file mode 100644
index 0000000..3bf1171
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Eucla differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Hobart b/telegramer/include/pytz/zoneinfo/Australia/Hobart
new file mode 100644
index 0000000..3adb8e1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Hobart differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/LHI b/telegramer/include/pytz/zoneinfo/Australia/LHI
new file mode 100644
index 0000000..9e04a80
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/LHI differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Lindeman b/telegramer/include/pytz/zoneinfo/Australia/Lindeman
new file mode 100644
index 0000000..4ee1825
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Lindeman differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Lord_Howe b/telegramer/include/pytz/zoneinfo/Australia/Lord_Howe
new file mode 100644
index 0000000..9e04a80
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Lord_Howe differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Melbourne b/telegramer/include/pytz/zoneinfo/Australia/Melbourne
new file mode 100644
index 0000000..ee903f4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Melbourne differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/NSW b/telegramer/include/pytz/zoneinfo/Australia/NSW
new file mode 100644
index 0000000..0aea4c3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/NSW differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/North b/telegramer/include/pytz/zoneinfo/Australia/North
new file mode 100644
index 0000000..74a3087
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/North differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Perth b/telegramer/include/pytz/zoneinfo/Australia/Perth
new file mode 100644
index 0000000..f8ddbdf
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Perth differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Queensland b/telegramer/include/pytz/zoneinfo/Australia/Queensland
new file mode 100644
index 0000000..7ff9949
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Queensland differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/South b/telegramer/include/pytz/zoneinfo/Australia/South
new file mode 100644
index 0000000..f5dedca
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/South differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Sydney b/telegramer/include/pytz/zoneinfo/Australia/Sydney
new file mode 100644
index 0000000..0aea4c3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Sydney differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Tasmania b/telegramer/include/pytz/zoneinfo/Australia/Tasmania
new file mode 100644
index 0000000..3adb8e1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Tasmania differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Victoria b/telegramer/include/pytz/zoneinfo/Australia/Victoria
new file mode 100644
index 0000000..ee903f4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Victoria differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/West b/telegramer/include/pytz/zoneinfo/Australia/West
new file mode 100644
index 0000000..f8ddbdf
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/West differ
diff --git a/telegramer/include/pytz/zoneinfo/Australia/Yancowinna b/telegramer/include/pytz/zoneinfo/Australia/Yancowinna
new file mode 100644
index 0000000..698c76e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Australia/Yancowinna differ
diff --git a/telegramer/include/pytz/zoneinfo/Brazil/Acre b/telegramer/include/pytz/zoneinfo/Brazil/Acre
new file mode 100644
index 0000000..a374cb4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Brazil/Acre differ
diff --git a/telegramer/include/pytz/zoneinfo/Brazil/DeNoronha b/telegramer/include/pytz/zoneinfo/Brazil/DeNoronha
new file mode 100644
index 0000000..f140726
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Brazil/DeNoronha differ
diff --git a/telegramer/include/pytz/zoneinfo/Brazil/East b/telegramer/include/pytz/zoneinfo/Brazil/East
new file mode 100644
index 0000000..13ff083
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Brazil/East differ
diff --git a/telegramer/include/pytz/zoneinfo/Brazil/West b/telegramer/include/pytz/zoneinfo/Brazil/West
new file mode 100644
index 0000000..63d58f8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Brazil/West differ
diff --git a/telegramer/include/pytz/zoneinfo/CET b/telegramer/include/pytz/zoneinfo/CET
new file mode 100644
index 0000000..122e934
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/CET differ
diff --git a/telegramer/include/pytz/zoneinfo/CST6CDT b/telegramer/include/pytz/zoneinfo/CST6CDT
new file mode 100644
index 0000000..ca67929
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/CST6CDT differ
diff --git a/telegramer/include/pytz/zoneinfo/Canada/Atlantic b/telegramer/include/pytz/zoneinfo/Canada/Atlantic
new file mode 100644
index 0000000..756099a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Atlantic differ
diff --git a/telegramer/include/pytz/zoneinfo/Canada/Central b/telegramer/include/pytz/zoneinfo/Canada/Central
new file mode 100644
index 0000000..ac40299
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Central differ
diff --git a/telegramer/include/pytz/zoneinfo/Canada/Eastern b/telegramer/include/pytz/zoneinfo/Canada/Eastern
new file mode 100644
index 0000000..6752c5b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Eastern differ
diff --git a/telegramer/include/pytz/zoneinfo/Canada/Mountain b/telegramer/include/pytz/zoneinfo/Canada/Mountain
new file mode 100644
index 0000000..cd78a6f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Mountain differ
diff --git a/telegramer/include/pytz/zoneinfo/Canada/Newfoundland b/telegramer/include/pytz/zoneinfo/Canada/Newfoundland
new file mode 100644
index 0000000..65a5b0c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Newfoundland differ
diff --git a/telegramer/include/pytz/zoneinfo/Canada/Pacific b/telegramer/include/pytz/zoneinfo/Canada/Pacific
new file mode 100644
index 0000000..bb60cbc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Pacific differ
diff --git a/telegramer/include/pytz/zoneinfo/Canada/Saskatchewan b/telegramer/include/pytz/zoneinfo/Canada/Saskatchewan
new file mode 100644
index 0000000..20c9c84
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Saskatchewan differ
diff --git a/telegramer/include/pytz/zoneinfo/Canada/Yukon b/telegramer/include/pytz/zoneinfo/Canada/Yukon
new file mode 100644
index 0000000..9ee229c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Canada/Yukon differ
diff --git a/telegramer/include/pytz/zoneinfo/Chile/Continental b/telegramer/include/pytz/zoneinfo/Chile/Continental
new file mode 100644
index 0000000..aa29060
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Chile/Continental differ
diff --git a/telegramer/include/pytz/zoneinfo/Chile/EasterIsland b/telegramer/include/pytz/zoneinfo/Chile/EasterIsland
new file mode 100644
index 0000000..cae3744
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Chile/EasterIsland differ
diff --git a/telegramer/include/pytz/zoneinfo/Cuba b/telegramer/include/pytz/zoneinfo/Cuba
new file mode 100644
index 0000000..b69ac45
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Cuba differ
diff --git a/telegramer/include/pytz/zoneinfo/EET b/telegramer/include/pytz/zoneinfo/EET
new file mode 100644
index 0000000..cbdb71d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/EET differ
diff --git a/telegramer/include/pytz/zoneinfo/EST b/telegramer/include/pytz/zoneinfo/EST
new file mode 100644
index 0000000..21ebc00
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/EST differ
diff --git a/telegramer/include/pytz/zoneinfo/EST5EDT b/telegramer/include/pytz/zoneinfo/EST5EDT
new file mode 100644
index 0000000..9bce500
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/EST5EDT differ
diff --git a/telegramer/include/pytz/zoneinfo/Egypt b/telegramer/include/pytz/zoneinfo/Egypt
new file mode 100644
index 0000000..d3f8196
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Egypt differ
diff --git a/telegramer/include/pytz/zoneinfo/Eire b/telegramer/include/pytz/zoneinfo/Eire
new file mode 100644
index 0000000..1d99490
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Eire differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT b/telegramer/include/pytz/zoneinfo/Etc/GMT
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+0 b/telegramer/include/pytz/zoneinfo/Etc/GMT+0
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+0 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+1 b/telegramer/include/pytz/zoneinfo/Etc/GMT+1
new file mode 100644
index 0000000..4dab6f9
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+1 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+10 b/telegramer/include/pytz/zoneinfo/Etc/GMT+10
new file mode 100644
index 0000000..c749290
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+10 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+11 b/telegramer/include/pytz/zoneinfo/Etc/GMT+11
new file mode 100644
index 0000000..d969982
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+11 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+12 b/telegramer/include/pytz/zoneinfo/Etc/GMT+12
new file mode 100644
index 0000000..cdeec90
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+12 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+2 b/telegramer/include/pytz/zoneinfo/Etc/GMT+2
new file mode 100644
index 0000000..fbd2a94
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+2 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+3 b/telegramer/include/pytz/zoneinfo/Etc/GMT+3
new file mode 100644
index 0000000..ee246ef
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+3 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+4 b/telegramer/include/pytz/zoneinfo/Etc/GMT+4
new file mode 100644
index 0000000..5a25ff2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+4 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+5 b/telegramer/include/pytz/zoneinfo/Etc/GMT+5
new file mode 100644
index 0000000..c0b745f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+5 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+6 b/telegramer/include/pytz/zoneinfo/Etc/GMT+6
new file mode 100644
index 0000000..06e777d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+6 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+7 b/telegramer/include/pytz/zoneinfo/Etc/GMT+7
new file mode 100644
index 0000000..4e0b53a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+7 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+8 b/telegramer/include/pytz/zoneinfo/Etc/GMT+8
new file mode 100644
index 0000000..714b0c5
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+8 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT+9 b/telegramer/include/pytz/zoneinfo/Etc/GMT+9
new file mode 100644
index 0000000..78b9daa
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT+9 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-0 b/telegramer/include/pytz/zoneinfo/Etc/GMT-0
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-0 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-1 b/telegramer/include/pytz/zoneinfo/Etc/GMT-1
new file mode 100644
index 0000000..a838beb
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-1 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-10 b/telegramer/include/pytz/zoneinfo/Etc/GMT-10
new file mode 100644
index 0000000..68ff77d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-10 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-11 b/telegramer/include/pytz/zoneinfo/Etc/GMT-11
new file mode 100644
index 0000000..66af5a4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-11 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-12 b/telegramer/include/pytz/zoneinfo/Etc/GMT-12
new file mode 100644
index 0000000..17ba505
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-12 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-13 b/telegramer/include/pytz/zoneinfo/Etc/GMT-13
new file mode 100644
index 0000000..5f3706c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-13 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-14 b/telegramer/include/pytz/zoneinfo/Etc/GMT-14
new file mode 100644
index 0000000..7e9f9c4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-14 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-2 b/telegramer/include/pytz/zoneinfo/Etc/GMT-2
new file mode 100644
index 0000000..fcef6d9
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-2 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-3 b/telegramer/include/pytz/zoneinfo/Etc/GMT-3
new file mode 100644
index 0000000..27973bc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-3 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-4 b/telegramer/include/pytz/zoneinfo/Etc/GMT-4
new file mode 100644
index 0000000..1efd841
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-4 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-5 b/telegramer/include/pytz/zoneinfo/Etc/GMT-5
new file mode 100644
index 0000000..1f76184
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-5 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-6 b/telegramer/include/pytz/zoneinfo/Etc/GMT-6
new file mode 100644
index 0000000..952681e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-6 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-7 b/telegramer/include/pytz/zoneinfo/Etc/GMT-7
new file mode 100644
index 0000000..cefc912
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-7 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-8 b/telegramer/include/pytz/zoneinfo/Etc/GMT-8
new file mode 100644
index 0000000..afb093d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-8 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT-9 b/telegramer/include/pytz/zoneinfo/Etc/GMT-9
new file mode 100644
index 0000000..9265fb7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT-9 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/GMT0 b/telegramer/include/pytz/zoneinfo/Etc/GMT0
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/GMT0 differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/Greenwich b/telegramer/include/pytz/zoneinfo/Etc/Greenwich
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/Greenwich differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/UCT b/telegramer/include/pytz/zoneinfo/Etc/UCT
new file mode 100644
index 0000000..91558be
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/UCT differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/UTC b/telegramer/include/pytz/zoneinfo/Etc/UTC
new file mode 100644
index 0000000..91558be
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/UTC differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/Universal b/telegramer/include/pytz/zoneinfo/Etc/Universal
new file mode 100644
index 0000000..91558be
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/Universal differ
diff --git a/telegramer/include/pytz/zoneinfo/Etc/Zulu b/telegramer/include/pytz/zoneinfo/Etc/Zulu
new file mode 100644
index 0000000..91558be
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Etc/Zulu differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Amsterdam b/telegramer/include/pytz/zoneinfo/Europe/Amsterdam
new file mode 100644
index 0000000..c3ff07b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Amsterdam differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Andorra b/telegramer/include/pytz/zoneinfo/Europe/Andorra
new file mode 100644
index 0000000..5962550
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Andorra differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Astrakhan b/telegramer/include/pytz/zoneinfo/Europe/Astrakhan
new file mode 100644
index 0000000..73a4d01
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Astrakhan differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Athens b/telegramer/include/pytz/zoneinfo/Europe/Athens
new file mode 100644
index 0000000..9f3a067
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Athens differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Belfast b/telegramer/include/pytz/zoneinfo/Europe/Belfast
new file mode 100644
index 0000000..ac02a81
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Belfast differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Belgrade b/telegramer/include/pytz/zoneinfo/Europe/Belgrade
new file mode 100644
index 0000000..27de456
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Belgrade differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Berlin b/telegramer/include/pytz/zoneinfo/Europe/Berlin
new file mode 100644
index 0000000..7f6d958
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Berlin differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Bratislava b/telegramer/include/pytz/zoneinfo/Europe/Bratislava
new file mode 100644
index 0000000..ce8f433
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Bratislava differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Brussels b/telegramer/include/pytz/zoneinfo/Europe/Brussels
new file mode 100644
index 0000000..40d7124
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Brussels differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Bucharest b/telegramer/include/pytz/zoneinfo/Europe/Bucharest
new file mode 100644
index 0000000..4303b90
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Bucharest differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Budapest b/telegramer/include/pytz/zoneinfo/Europe/Budapest
new file mode 100644
index 0000000..b76c873
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Budapest differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Busingen b/telegramer/include/pytz/zoneinfo/Europe/Busingen
new file mode 100644
index 0000000..ad6cf59
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Busingen differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Chisinau b/telegramer/include/pytz/zoneinfo/Europe/Chisinau
new file mode 100644
index 0000000..5ee23fe
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Chisinau differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Copenhagen b/telegramer/include/pytz/zoneinfo/Europe/Copenhagen
new file mode 100644
index 0000000..776be6e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Copenhagen differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Dublin b/telegramer/include/pytz/zoneinfo/Europe/Dublin
new file mode 100644
index 0000000..1d99490
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Dublin differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Gibraltar b/telegramer/include/pytz/zoneinfo/Europe/Gibraltar
new file mode 100644
index 0000000..117aadb
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Gibraltar differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Guernsey b/telegramer/include/pytz/zoneinfo/Europe/Guernsey
new file mode 100644
index 0000000..ac02a81
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Guernsey differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Helsinki b/telegramer/include/pytz/zoneinfo/Europe/Helsinki
new file mode 100644
index 0000000..b4f8f9c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Helsinki differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Isle_of_Man b/telegramer/include/pytz/zoneinfo/Europe/Isle_of_Man
new file mode 100644
index 0000000..ac02a81
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Isle_of_Man differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Istanbul b/telegramer/include/pytz/zoneinfo/Europe/Istanbul
new file mode 100644
index 0000000..508446b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Istanbul differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Jersey b/telegramer/include/pytz/zoneinfo/Europe/Jersey
new file mode 100644
index 0000000..ac02a81
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Jersey differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Kaliningrad b/telegramer/include/pytz/zoneinfo/Europe/Kaliningrad
new file mode 100644
index 0000000..cc99bea
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Kaliningrad differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Kiev b/telegramer/include/pytz/zoneinfo/Europe/Kiev
new file mode 100644
index 0000000..52efea8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Kiev differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Kirov b/telegramer/include/pytz/zoneinfo/Europe/Kirov
new file mode 100644
index 0000000..a3b5320
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Kirov differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Lisbon b/telegramer/include/pytz/zoneinfo/Europe/Lisbon
new file mode 100644
index 0000000..55f0193
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Lisbon differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Ljubljana b/telegramer/include/pytz/zoneinfo/Europe/Ljubljana
new file mode 100644
index 0000000..27de456
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Ljubljana differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/London b/telegramer/include/pytz/zoneinfo/Europe/London
new file mode 100644
index 0000000..ac02a81
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/London differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Luxembourg b/telegramer/include/pytz/zoneinfo/Europe/Luxembourg
new file mode 100644
index 0000000..c4ca733
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Luxembourg differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Madrid b/telegramer/include/pytz/zoneinfo/Europe/Madrid
new file mode 100644
index 0000000..16f6420
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Madrid differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Malta b/telegramer/include/pytz/zoneinfo/Europe/Malta
new file mode 100644
index 0000000..bf2452d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Malta differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Mariehamn b/telegramer/include/pytz/zoneinfo/Europe/Mariehamn
new file mode 100644
index 0000000..b4f8f9c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Mariehamn differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Minsk b/telegramer/include/pytz/zoneinfo/Europe/Minsk
new file mode 100644
index 0000000..453306c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Minsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Monaco b/telegramer/include/pytz/zoneinfo/Europe/Monaco
new file mode 100644
index 0000000..adbe45d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Monaco differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Moscow b/telegramer/include/pytz/zoneinfo/Europe/Moscow
new file mode 100644
index 0000000..ddb3f4e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Moscow differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Nicosia b/telegramer/include/pytz/zoneinfo/Europe/Nicosia
new file mode 100644
index 0000000..f7f10ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Nicosia differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Oslo b/telegramer/include/pytz/zoneinfo/Europe/Oslo
new file mode 100644
index 0000000..15a34c3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Oslo differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Paris b/telegramer/include/pytz/zoneinfo/Europe/Paris
new file mode 100644
index 0000000..7d366c6
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Paris differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Podgorica b/telegramer/include/pytz/zoneinfo/Europe/Podgorica
new file mode 100644
index 0000000..27de456
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Podgorica differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Prague b/telegramer/include/pytz/zoneinfo/Europe/Prague
new file mode 100644
index 0000000..ce8f433
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Prague differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Riga b/telegramer/include/pytz/zoneinfo/Europe/Riga
new file mode 100644
index 0000000..8db477d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Riga differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Rome b/telegramer/include/pytz/zoneinfo/Europe/Rome
new file mode 100644
index 0000000..ac4c163
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Rome differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Samara b/telegramer/include/pytz/zoneinfo/Europe/Samara
new file mode 100644
index 0000000..97d5dd9
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Samara differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/San_Marino b/telegramer/include/pytz/zoneinfo/Europe/San_Marino
new file mode 100644
index 0000000..ac4c163
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/San_Marino differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Sarajevo b/telegramer/include/pytz/zoneinfo/Europe/Sarajevo
new file mode 100644
index 0000000..27de456
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Sarajevo differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Saratov b/telegramer/include/pytz/zoneinfo/Europe/Saratov
new file mode 100644
index 0000000..8fd5f6d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Saratov differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Simferopol b/telegramer/include/pytz/zoneinfo/Europe/Simferopol
new file mode 100644
index 0000000..29107a0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Simferopol differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Skopje b/telegramer/include/pytz/zoneinfo/Europe/Skopje
new file mode 100644
index 0000000..27de456
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Skopje differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Sofia b/telegramer/include/pytz/zoneinfo/Europe/Sofia
new file mode 100644
index 0000000..0e4d879
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Sofia differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Stockholm b/telegramer/include/pytz/zoneinfo/Europe/Stockholm
new file mode 100644
index 0000000..f3e0c7f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Stockholm differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Tallinn b/telegramer/include/pytz/zoneinfo/Europe/Tallinn
new file mode 100644
index 0000000..b5acca3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Tallinn differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Tirane b/telegramer/include/pytz/zoneinfo/Europe/Tirane
new file mode 100644
index 0000000..0b86017
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Tirane differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Tiraspol b/telegramer/include/pytz/zoneinfo/Europe/Tiraspol
new file mode 100644
index 0000000..5ee23fe
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Tiraspol differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Ulyanovsk b/telegramer/include/pytz/zoneinfo/Europe/Ulyanovsk
new file mode 100644
index 0000000..7b61bdc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Ulyanovsk differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Uzhgorod b/telegramer/include/pytz/zoneinfo/Europe/Uzhgorod
new file mode 100644
index 0000000..0eb2150
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Uzhgorod differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Vaduz b/telegramer/include/pytz/zoneinfo/Europe/Vaduz
new file mode 100644
index 0000000..ad6cf59
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Vaduz differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Vatican b/telegramer/include/pytz/zoneinfo/Europe/Vatican
new file mode 100644
index 0000000..ac4c163
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Vatican differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Vienna b/telegramer/include/pytz/zoneinfo/Europe/Vienna
new file mode 100644
index 0000000..3582bb1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Vienna differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Vilnius b/telegramer/include/pytz/zoneinfo/Europe/Vilnius
new file mode 100644
index 0000000..7abd63f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Vilnius differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Volgograd b/telegramer/include/pytz/zoneinfo/Europe/Volgograd
new file mode 100644
index 0000000..11739ac
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Volgograd differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Warsaw b/telegramer/include/pytz/zoneinfo/Europe/Warsaw
new file mode 100644
index 0000000..e33cf67
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Warsaw differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Zagreb b/telegramer/include/pytz/zoneinfo/Europe/Zagreb
new file mode 100644
index 0000000..27de456
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Zagreb differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Zaporozhye b/telegramer/include/pytz/zoneinfo/Europe/Zaporozhye
new file mode 100644
index 0000000..f0406c1
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Zaporozhye differ
diff --git a/telegramer/include/pytz/zoneinfo/Europe/Zurich b/telegramer/include/pytz/zoneinfo/Europe/Zurich
new file mode 100644
index 0000000..ad6cf59
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Europe/Zurich differ
diff --git a/telegramer/include/pytz/zoneinfo/Factory b/telegramer/include/pytz/zoneinfo/Factory
new file mode 100644
index 0000000..60aa2a0
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Factory differ
diff --git a/telegramer/include/pytz/zoneinfo/GB b/telegramer/include/pytz/zoneinfo/GB
new file mode 100644
index 0000000..ac02a81
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GB differ
diff --git a/telegramer/include/pytz/zoneinfo/GB-Eire b/telegramer/include/pytz/zoneinfo/GB-Eire
new file mode 100644
index 0000000..ac02a81
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GB-Eire differ
diff --git a/telegramer/include/pytz/zoneinfo/GMT b/telegramer/include/pytz/zoneinfo/GMT
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GMT differ
diff --git a/telegramer/include/pytz/zoneinfo/GMT+0 b/telegramer/include/pytz/zoneinfo/GMT+0
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GMT+0 differ
diff --git a/telegramer/include/pytz/zoneinfo/GMT-0 b/telegramer/include/pytz/zoneinfo/GMT-0
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GMT-0 differ
diff --git a/telegramer/include/pytz/zoneinfo/GMT0 b/telegramer/include/pytz/zoneinfo/GMT0
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/GMT0 differ
diff --git a/telegramer/include/pytz/zoneinfo/Greenwich b/telegramer/include/pytz/zoneinfo/Greenwich
new file mode 100644
index 0000000..c634746
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Greenwich differ
diff --git a/telegramer/include/pytz/zoneinfo/HST b/telegramer/include/pytz/zoneinfo/HST
new file mode 100644
index 0000000..cccd45e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/HST differ
diff --git a/telegramer/include/pytz/zoneinfo/Hongkong b/telegramer/include/pytz/zoneinfo/Hongkong
new file mode 100644
index 0000000..23d0375
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Hongkong differ
diff --git a/telegramer/include/pytz/zoneinfo/Iceland b/telegramer/include/pytz/zoneinfo/Iceland
new file mode 100644
index 0000000..10e0fc8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Iceland differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Antananarivo b/telegramer/include/pytz/zoneinfo/Indian/Antananarivo
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Antananarivo differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Chagos b/telegramer/include/pytz/zoneinfo/Indian/Chagos
new file mode 100644
index 0000000..93d6dda
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Chagos differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Christmas b/telegramer/include/pytz/zoneinfo/Indian/Christmas
new file mode 100644
index 0000000..d18c381
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Christmas differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Cocos b/telegramer/include/pytz/zoneinfo/Indian/Cocos
new file mode 100644
index 0000000..f8116e7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Cocos differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Comoro b/telegramer/include/pytz/zoneinfo/Indian/Comoro
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Comoro differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Kerguelen b/telegramer/include/pytz/zoneinfo/Indian/Kerguelen
new file mode 100644
index 0000000..cde4cf7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Kerguelen differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Mahe b/telegramer/include/pytz/zoneinfo/Indian/Mahe
new file mode 100644
index 0000000..208f938
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Mahe differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Maldives b/telegramer/include/pytz/zoneinfo/Indian/Maldives
new file mode 100644
index 0000000..7c839cf
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Maldives differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Mauritius b/telegramer/include/pytz/zoneinfo/Indian/Mauritius
new file mode 100644
index 0000000..17f2616
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Mauritius differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Mayotte b/telegramer/include/pytz/zoneinfo/Indian/Mayotte
new file mode 100644
index 0000000..9dcfc19
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Mayotte differ
diff --git a/telegramer/include/pytz/zoneinfo/Indian/Reunion b/telegramer/include/pytz/zoneinfo/Indian/Reunion
new file mode 100644
index 0000000..dfe0831
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Indian/Reunion differ
diff --git a/telegramer/include/pytz/zoneinfo/Iran b/telegramer/include/pytz/zoneinfo/Iran
new file mode 100644
index 0000000..8cec5ad
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Iran differ
diff --git a/telegramer/include/pytz/zoneinfo/Israel b/telegramer/include/pytz/zoneinfo/Israel
new file mode 100644
index 0000000..1ebd066
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Israel differ
diff --git a/telegramer/include/pytz/zoneinfo/Jamaica b/telegramer/include/pytz/zoneinfo/Jamaica
new file mode 100644
index 0000000..2a9b7fd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Jamaica differ
diff --git a/telegramer/include/pytz/zoneinfo/Japan b/telegramer/include/pytz/zoneinfo/Japan
new file mode 100644
index 0000000..26f4d34
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Japan differ
diff --git a/telegramer/include/pytz/zoneinfo/Kwajalein b/telegramer/include/pytz/zoneinfo/Kwajalein
new file mode 100644
index 0000000..1a7975f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Kwajalein differ
diff --git a/telegramer/include/pytz/zoneinfo/Libya b/telegramer/include/pytz/zoneinfo/Libya
new file mode 100644
index 0000000..07b393b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Libya differ
diff --git a/telegramer/include/pytz/zoneinfo/MET b/telegramer/include/pytz/zoneinfo/MET
new file mode 100644
index 0000000..4a826bb
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/MET differ
diff --git a/telegramer/include/pytz/zoneinfo/MST b/telegramer/include/pytz/zoneinfo/MST
new file mode 100644
index 0000000..c93a58e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/MST differ
diff --git a/telegramer/include/pytz/zoneinfo/MST7MDT b/telegramer/include/pytz/zoneinfo/MST7MDT
new file mode 100644
index 0000000..4506a6e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/MST7MDT differ
diff --git a/telegramer/include/pytz/zoneinfo/Mexico/BajaNorte b/telegramer/include/pytz/zoneinfo/Mexico/BajaNorte
new file mode 100644
index 0000000..ada6bf7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Mexico/BajaNorte differ
diff --git a/telegramer/include/pytz/zoneinfo/Mexico/BajaSur b/telegramer/include/pytz/zoneinfo/Mexico/BajaSur
new file mode 100644
index 0000000..e4a7857
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Mexico/BajaSur differ
diff --git a/telegramer/include/pytz/zoneinfo/Mexico/General b/telegramer/include/pytz/zoneinfo/Mexico/General
new file mode 100644
index 0000000..e7fb6f2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Mexico/General differ
diff --git a/telegramer/include/pytz/zoneinfo/NZ b/telegramer/include/pytz/zoneinfo/NZ
new file mode 100644
index 0000000..6575fdc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/NZ differ
diff --git a/telegramer/include/pytz/zoneinfo/NZ-CHAT b/telegramer/include/pytz/zoneinfo/NZ-CHAT
new file mode 100644
index 0000000..c004109
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/NZ-CHAT differ
diff --git a/telegramer/include/pytz/zoneinfo/Navajo b/telegramer/include/pytz/zoneinfo/Navajo
new file mode 100644
index 0000000..5fbe26b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Navajo differ
diff --git a/telegramer/include/pytz/zoneinfo/PRC b/telegramer/include/pytz/zoneinfo/PRC
new file mode 100644
index 0000000..91f6f8b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/PRC differ
diff --git a/telegramer/include/pytz/zoneinfo/PST8PDT b/telegramer/include/pytz/zoneinfo/PST8PDT
new file mode 100644
index 0000000..99d246b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/PST8PDT differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Apia b/telegramer/include/pytz/zoneinfo/Pacific/Apia
new file mode 100644
index 0000000..999c367
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Apia differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Auckland b/telegramer/include/pytz/zoneinfo/Pacific/Auckland
new file mode 100644
index 0000000..6575fdc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Auckland differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Bougainville b/telegramer/include/pytz/zoneinfo/Pacific/Bougainville
new file mode 100644
index 0000000..2892d26
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Bougainville differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Chatham b/telegramer/include/pytz/zoneinfo/Pacific/Chatham
new file mode 100644
index 0000000..c004109
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Chatham differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Chuuk b/telegramer/include/pytz/zoneinfo/Pacific/Chuuk
new file mode 100644
index 0000000..07c84b7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Chuuk differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Easter b/telegramer/include/pytz/zoneinfo/Pacific/Easter
new file mode 100644
index 0000000..cae3744
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Easter differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Efate b/telegramer/include/pytz/zoneinfo/Pacific/Efate
new file mode 100644
index 0000000..d8d4093
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Efate differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Enderbury b/telegramer/include/pytz/zoneinfo/Pacific/Enderbury
new file mode 100644
index 0000000..39b786e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Enderbury differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Fakaofo b/telegramer/include/pytz/zoneinfo/Pacific/Fakaofo
new file mode 100644
index 0000000..e40307f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Fakaofo differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Fiji b/telegramer/include/pytz/zoneinfo/Pacific/Fiji
new file mode 100644
index 0000000..af07ac8
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Fiji differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Funafuti b/telegramer/include/pytz/zoneinfo/Pacific/Funafuti
new file mode 100644
index 0000000..ea72863
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Funafuti differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Galapagos b/telegramer/include/pytz/zoneinfo/Pacific/Galapagos
new file mode 100644
index 0000000..31f0921
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Galapagos differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Gambier b/telegramer/include/pytz/zoneinfo/Pacific/Gambier
new file mode 100644
index 0000000..e1fc3da
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Gambier differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Guadalcanal b/telegramer/include/pytz/zoneinfo/Pacific/Guadalcanal
new file mode 100644
index 0000000..7e9d10a
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Guadalcanal differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Guam b/telegramer/include/pytz/zoneinfo/Pacific/Guam
new file mode 100644
index 0000000..66490d2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Guam differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Honolulu b/telegramer/include/pytz/zoneinfo/Pacific/Honolulu
new file mode 100644
index 0000000..c7cd060
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Honolulu differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Johnston b/telegramer/include/pytz/zoneinfo/Pacific/Johnston
new file mode 100644
index 0000000..c7cd060
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Johnston differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Kanton b/telegramer/include/pytz/zoneinfo/Pacific/Kanton
new file mode 100644
index 0000000..39b786e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Kanton differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Kiritimati b/telegramer/include/pytz/zoneinfo/Pacific/Kiritimati
new file mode 100644
index 0000000..7cae0cb
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Kiritimati differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Kosrae b/telegramer/include/pytz/zoneinfo/Pacific/Kosrae
new file mode 100644
index 0000000..a584aae
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Kosrae differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Kwajalein b/telegramer/include/pytz/zoneinfo/Pacific/Kwajalein
new file mode 100644
index 0000000..1a7975f
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Kwajalein differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Majuro b/telegramer/include/pytz/zoneinfo/Pacific/Majuro
new file mode 100644
index 0000000..9ef8374
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Majuro differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Marquesas b/telegramer/include/pytz/zoneinfo/Pacific/Marquesas
new file mode 100644
index 0000000..74d6792
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Marquesas differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Midway b/telegramer/include/pytz/zoneinfo/Pacific/Midway
new file mode 100644
index 0000000..cb56709
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Midway differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Nauru b/telegramer/include/pytz/zoneinfo/Pacific/Nauru
new file mode 100644
index 0000000..acec042
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Nauru differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Niue b/telegramer/include/pytz/zoneinfo/Pacific/Niue
new file mode 100644
index 0000000..89117b3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Niue differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Norfolk b/telegramer/include/pytz/zoneinfo/Pacific/Norfolk
new file mode 100644
index 0000000..53c1aad
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Norfolk differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Noumea b/telegramer/include/pytz/zoneinfo/Pacific/Noumea
new file mode 100644
index 0000000..931a1a3
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Noumea differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Pago_Pago b/telegramer/include/pytz/zoneinfo/Pacific/Pago_Pago
new file mode 100644
index 0000000..cb56709
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Pago_Pago differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Palau b/telegramer/include/pytz/zoneinfo/Pacific/Palau
new file mode 100644
index 0000000..146b351
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Palau differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Pitcairn b/telegramer/include/pytz/zoneinfo/Pacific/Pitcairn
new file mode 100644
index 0000000..ef91b06
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Pitcairn differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Pohnpei b/telegramer/include/pytz/zoneinfo/Pacific/Pohnpei
new file mode 100644
index 0000000..c298ddd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Pohnpei differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Ponape b/telegramer/include/pytz/zoneinfo/Pacific/Ponape
new file mode 100644
index 0000000..c298ddd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Ponape differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Port_Moresby b/telegramer/include/pytz/zoneinfo/Pacific/Port_Moresby
new file mode 100644
index 0000000..920ad27
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Port_Moresby differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Rarotonga b/telegramer/include/pytz/zoneinfo/Pacific/Rarotonga
new file mode 100644
index 0000000..eea37ab
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Rarotonga differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Saipan b/telegramer/include/pytz/zoneinfo/Pacific/Saipan
new file mode 100644
index 0000000..66490d2
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Saipan differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Samoa b/telegramer/include/pytz/zoneinfo/Pacific/Samoa
new file mode 100644
index 0000000..cb56709
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Samoa differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Tahiti b/telegramer/include/pytz/zoneinfo/Pacific/Tahiti
new file mode 100644
index 0000000..442b8eb
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Tahiti differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Tarawa b/telegramer/include/pytz/zoneinfo/Pacific/Tarawa
new file mode 100644
index 0000000..3db6c75
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Tarawa differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Tongatapu b/telegramer/include/pytz/zoneinfo/Pacific/Tongatapu
new file mode 100644
index 0000000..c2e5999
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Tongatapu differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Truk b/telegramer/include/pytz/zoneinfo/Pacific/Truk
new file mode 100644
index 0000000..07c84b7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Truk differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Wake b/telegramer/include/pytz/zoneinfo/Pacific/Wake
new file mode 100644
index 0000000..c9e3106
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Wake differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Wallis b/telegramer/include/pytz/zoneinfo/Pacific/Wallis
new file mode 100644
index 0000000..b35344b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Wallis differ
diff --git a/telegramer/include/pytz/zoneinfo/Pacific/Yap b/telegramer/include/pytz/zoneinfo/Pacific/Yap
new file mode 100644
index 0000000..07c84b7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Pacific/Yap differ
diff --git a/telegramer/include/pytz/zoneinfo/Poland b/telegramer/include/pytz/zoneinfo/Poland
new file mode 100644
index 0000000..e33cf67
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Poland differ
diff --git a/telegramer/include/pytz/zoneinfo/Portugal b/telegramer/include/pytz/zoneinfo/Portugal
new file mode 100644
index 0000000..55f0193
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Portugal differ
diff --git a/telegramer/include/pytz/zoneinfo/ROC b/telegramer/include/pytz/zoneinfo/ROC
new file mode 100644
index 0000000..24c4344
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/ROC differ
diff --git a/telegramer/include/pytz/zoneinfo/ROK b/telegramer/include/pytz/zoneinfo/ROK
new file mode 100644
index 0000000..96199e7
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/ROK differ
diff --git a/telegramer/include/pytz/zoneinfo/Singapore b/telegramer/include/pytz/zoneinfo/Singapore
new file mode 100644
index 0000000..2364b21
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Singapore differ
diff --git a/telegramer/include/pytz/zoneinfo/Turkey b/telegramer/include/pytz/zoneinfo/Turkey
new file mode 100644
index 0000000..508446b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Turkey differ
diff --git a/telegramer/include/pytz/zoneinfo/UCT b/telegramer/include/pytz/zoneinfo/UCT
new file mode 100644
index 0000000..91558be
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/UCT differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Alaska b/telegramer/include/pytz/zoneinfo/US/Alaska
new file mode 100644
index 0000000..9bbb2fd
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Alaska differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Aleutian b/telegramer/include/pytz/zoneinfo/US/Aleutian
new file mode 100644
index 0000000..4323649
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Aleutian differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Arizona b/telegramer/include/pytz/zoneinfo/US/Arizona
new file mode 100644
index 0000000..ac6bb0c
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Arizona differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Central b/telegramer/include/pytz/zoneinfo/US/Central
new file mode 100644
index 0000000..a5b1617
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Central differ
diff --git a/telegramer/include/pytz/zoneinfo/US/East-Indiana b/telegramer/include/pytz/zoneinfo/US/East-Indiana
new file mode 100644
index 0000000..09511cc
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/East-Indiana differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Eastern b/telegramer/include/pytz/zoneinfo/US/Eastern
new file mode 100644
index 0000000..2f75480
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Eastern differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Hawaii b/telegramer/include/pytz/zoneinfo/US/Hawaii
new file mode 100644
index 0000000..c7cd060
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Hawaii differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Indiana-Starke b/telegramer/include/pytz/zoneinfo/US/Indiana-Starke
new file mode 100644
index 0000000..fcd408d
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Indiana-Starke differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Michigan b/telegramer/include/pytz/zoneinfo/US/Michigan
new file mode 100644
index 0000000..e104faa
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Michigan differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Mountain b/telegramer/include/pytz/zoneinfo/US/Mountain
new file mode 100644
index 0000000..5fbe26b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Mountain differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Pacific b/telegramer/include/pytz/zoneinfo/US/Pacific
new file mode 100644
index 0000000..9dad4f4
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Pacific differ
diff --git a/telegramer/include/pytz/zoneinfo/US/Samoa b/telegramer/include/pytz/zoneinfo/US/Samoa
new file mode 100644
index 0000000..cb56709
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/US/Samoa differ
diff --git a/telegramer/include/pytz/zoneinfo/UTC b/telegramer/include/pytz/zoneinfo/UTC
new file mode 100644
index 0000000..91558be
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/UTC differ
diff --git a/telegramer/include/pytz/zoneinfo/Universal b/telegramer/include/pytz/zoneinfo/Universal
new file mode 100644
index 0000000..91558be
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Universal differ
diff --git a/telegramer/include/pytz/zoneinfo/W-SU b/telegramer/include/pytz/zoneinfo/W-SU
new file mode 100644
index 0000000..ddb3f4e
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/W-SU differ
diff --git a/telegramer/include/pytz/zoneinfo/WET b/telegramer/include/pytz/zoneinfo/WET
new file mode 100644
index 0000000..c27390b
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/WET differ
diff --git a/telegramer/include/pytz/zoneinfo/Zulu b/telegramer/include/pytz/zoneinfo/Zulu
new file mode 100644
index 0000000..91558be
Binary files /dev/null and b/telegramer/include/pytz/zoneinfo/Zulu differ
diff --git a/telegramer/include/pytz/zoneinfo/iso3166.tab b/telegramer/include/pytz/zoneinfo/iso3166.tab
new file mode 100644
index 0000000..a4ff61a
--- /dev/null
+++ b/telegramer/include/pytz/zoneinfo/iso3166.tab
@@ -0,0 +1,274 @@
+# ISO 3166 alpha-2 country codes
+#
+# This file is in the public domain, so clarified as of
+# 2009-05-17 by Arthur David Olson.
+#
+# From Paul Eggert (2015-05-02):
+# This file contains a table of two-letter country codes. Columns are
+# separated by a single tab. Lines beginning with '#' are comments.
+# All text uses UTF-8 encoding. The columns of the table are as follows:
+#
+# 1. ISO 3166-1 alpha-2 country code, current as of
+# ISO 3166-1 N976 (2018-11-06). See: Updates on ISO 3166-1
+# https://isotc.iso.org/livelink/livelink/Open/16944257
+# 2. The usual English name for the coded region,
+# chosen so that alphabetic sorting of subsets produces helpful lists.
+# This is not the same as the English name in the ISO 3166 tables.
+#
+# The table is sorted by country code.
+#
+# This table is intended as an aid for users, to help them select time
+# zone data appropriate for their practical needs. It is not intended
+# to take or endorse any position on legal or territorial claims.
+#
+#country-
+#code name of country, territory, area, or subdivision
+AD Andorra
+AE United Arab Emirates
+AF Afghanistan
+AG Antigua & Barbuda
+AI Anguilla
+AL Albania
+AM Armenia
+AO Angola
+AQ Antarctica
+AR Argentina
+AS Samoa (American)
+AT Austria
+AU Australia
+AW Aruba
+AX Åland Islands
+AZ Azerbaijan
+BA Bosnia & Herzegovina
+BB Barbados
+BD Bangladesh
+BE Belgium
+BF Burkina Faso
+BG Bulgaria
+BH Bahrain
+BI Burundi
+BJ Benin
+BL St Barthelemy
+BM Bermuda
+BN Brunei
+BO Bolivia
+BQ Caribbean NL
+BR Brazil
+BS Bahamas
+BT Bhutan
+BV Bouvet Island
+BW Botswana
+BY Belarus
+BZ Belize
+CA Canada
+CC Cocos (Keeling) Islands
+CD Congo (Dem. Rep.)
+CF Central African Rep.
+CG Congo (Rep.)
+CH Switzerland
+CI Côte d'Ivoire
+CK Cook Islands
+CL Chile
+CM Cameroon
+CN China
+CO Colombia
+CR Costa Rica
+CU Cuba
+CV Cape Verde
+CW Curaçao
+CX Christmas Island
+CY Cyprus
+CZ Czech Republic
+DE Germany
+DJ Djibouti
+DK Denmark
+DM Dominica
+DO Dominican Republic
+DZ Algeria
+EC Ecuador
+EE Estonia
+EG Egypt
+EH Western Sahara
+ER Eritrea
+ES Spain
+ET Ethiopia
+FI Finland
+FJ Fiji
+FK Falkland Islands
+FM Micronesia
+FO Faroe Islands
+FR France
+GA Gabon
+GB Britain (UK)
+GD Grenada
+GE Georgia
+GF French Guiana
+GG Guernsey
+GH Ghana
+GI Gibraltar
+GL Greenland
+GM Gambia
+GN Guinea
+GP Guadeloupe
+GQ Equatorial Guinea
+GR Greece
+GS South Georgia & the South Sandwich Islands
+GT Guatemala
+GU Guam
+GW Guinea-Bissau
+GY Guyana
+HK Hong Kong
+HM Heard Island & McDonald Islands
+HN Honduras
+HR Croatia
+HT Haiti
+HU Hungary
+ID Indonesia
+IE Ireland
+IL Israel
+IM Isle of Man
+IN India
+IO British Indian Ocean Territory
+IQ Iraq
+IR Iran
+IS Iceland
+IT Italy
+JE Jersey
+JM Jamaica
+JO Jordan
+JP Japan
+KE Kenya
+KG Kyrgyzstan
+KH Cambodia
+KI Kiribati
+KM Comoros
+KN St Kitts & Nevis
+KP Korea (North)
+KR Korea (South)
+KW Kuwait
+KY Cayman Islands
+KZ Kazakhstan
+LA Laos
+LB Lebanon
+LC St Lucia
+LI Liechtenstein
+LK Sri Lanka
+LR Liberia
+LS Lesotho
+LT Lithuania
+LU Luxembourg
+LV Latvia
+LY Libya
+MA Morocco
+MC Monaco
+MD Moldova
+ME Montenegro
+MF St Martin (French)
+MG Madagascar
+MH Marshall Islands
+MK North Macedonia
+ML Mali
+MM Myanmar (Burma)
+MN Mongolia
+MO Macau
+MP Northern Mariana Islands
+MQ Martinique
+MR Mauritania
+MS Montserrat
+MT Malta
+MU Mauritius
+MV Maldives
+MW Malawi
+MX Mexico
+MY Malaysia
+MZ Mozambique
+NA Namibia
+NC New Caledonia
+NE Niger
+NF Norfolk Island
+NG Nigeria
+NI Nicaragua
+NL Netherlands
+NO Norway
+NP Nepal
+NR Nauru
+NU Niue
+NZ New Zealand
+OM Oman
+PA Panama
+PE Peru
+PF French Polynesia
+PG Papua New Guinea
+PH Philippines
+PK Pakistan
+PL Poland
+PM St Pierre & Miquelon
+PN Pitcairn
+PR Puerto Rico
+PS Palestine
+PT Portugal
+PW Palau
+PY Paraguay
+QA Qatar
+RE Réunion
+RO Romania
+RS Serbia
+RU Russia
+RW Rwanda
+SA Saudi Arabia
+SB Solomon Islands
+SC Seychelles
+SD Sudan
+SE Sweden
+SG Singapore
+SH St Helena
+SI Slovenia
+SJ Svalbard & Jan Mayen
+SK Slovakia
+SL Sierra Leone
+SM San Marino
+SN Senegal
+SO Somalia
+SR Suriname
+SS South Sudan
+ST Sao Tome & Principe
+SV El Salvador
+SX St Maarten (Dutch)
+SY Syria
+SZ Eswatini (Swaziland)
+TC Turks & Caicos Is
+TD Chad
+TF French Southern & Antarctic Lands
+TG Togo
+TH Thailand
+TJ Tajikistan
+TK Tokelau
+TL East Timor
+TM Turkmenistan
+TN Tunisia
+TO Tonga
+TR Turkey
+TT Trinidad & Tobago
+TV Tuvalu
+TW Taiwan
+TZ Tanzania
+UA Ukraine
+UG Uganda
+UM US minor outlying islands
+US United States
+UY Uruguay
+UZ Uzbekistan
+VA Vatican City
+VC St Vincent
+VE Venezuela
+VG Virgin Islands (UK)
+VI Virgin Islands (US)
+VN Vietnam
+VU Vanuatu
+WF Wallis & Futuna
+WS Samoa (western)
+YE Yemen
+YT Mayotte
+ZA South Africa
+ZM Zambia
+ZW Zimbabwe
diff --git a/telegramer/include/pytz/zoneinfo/leapseconds b/telegramer/include/pytz/zoneinfo/leapseconds
new file mode 100644
index 0000000..ffa5eb8
--- /dev/null
+++ b/telegramer/include/pytz/zoneinfo/leapseconds
@@ -0,0 +1,82 @@
+# Allowance for leap seconds added to each time zone file.
+
+# This file is in the public domain.
+
+# This file is generated automatically from the data in the public-domain
+# NIST format leap-seconds.list file, which can be copied from
+#
+# or .
+# The NIST file is used instead of its IERS upstream counterpart
+#
+# because under US law the NIST file is public domain
+# whereas the IERS file's copyright and license status is unclear.
+# For more about leap-seconds.list, please see
+# The NTP Timescale and Leap Seconds
+# .
+
+# The rules for leap seconds are specified in Annex 1 (Time scales) of:
+# Standard-frequency and time-signal emissions.
+# International Telecommunication Union - Radiocommunication Sector
+# (ITU-R) Recommendation TF.460-6 (02/2002)
+# .
+# The International Earth Rotation and Reference Systems Service (IERS)
+# periodically uses leap seconds to keep UTC to within 0.9 s of UT1
+# (a proxy for Earth's angle in space as measured by astronomers)
+# and publishes leap second data in a copyrighted file
+# .
+# See: Levine J. Coordinated Universal Time and the leap second.
+# URSI Radio Sci Bull. 2016;89(4):30-6. doi:10.23919/URSIRSB.2016.7909995
+# .
+
+# There were no leap seconds before 1972, as no official mechanism
+# accounted for the discrepancy between atomic time (TAI) and the earth's
+# rotation. The first ("1 Jan 1972") data line in leap-seconds.list
+# does not denote a leap second; it denotes the start of the current definition
+# of UTC.
+
+# All leap-seconds are Stationary (S) at the given UTC time.
+# The correction (+ or -) is made at the given time, so in the unlikely
+# event of a negative leap second, a line would look like this:
+# Leap YEAR MON DAY 23:59:59 - S
+# Typical lines look like this:
+# Leap YEAR MON DAY 23:59:60 + S
+Leap 1972 Jun 30 23:59:60 + S
+Leap 1972 Dec 31 23:59:60 + S
+Leap 1973 Dec 31 23:59:60 + S
+Leap 1974 Dec 31 23:59:60 + S
+Leap 1975 Dec 31 23:59:60 + S
+Leap 1976 Dec 31 23:59:60 + S
+Leap 1977 Dec 31 23:59:60 + S
+Leap 1978 Dec 31 23:59:60 + S
+Leap 1979 Dec 31 23:59:60 + S
+Leap 1981 Jun 30 23:59:60 + S
+Leap 1982 Jun 30 23:59:60 + S
+Leap 1983 Jun 30 23:59:60 + S
+Leap 1985 Jun 30 23:59:60 + S
+Leap 1987 Dec 31 23:59:60 + S
+Leap 1989 Dec 31 23:59:60 + S
+Leap 1990 Dec 31 23:59:60 + S
+Leap 1992 Jun 30 23:59:60 + S
+Leap 1993 Jun 30 23:59:60 + S
+Leap 1994 Jun 30 23:59:60 + S
+Leap 1995 Dec 31 23:59:60 + S
+Leap 1997 Jun 30 23:59:60 + S
+Leap 1998 Dec 31 23:59:60 + S
+Leap 2005 Dec 31 23:59:60 + S
+Leap 2008 Dec 31 23:59:60 + S
+Leap 2012 Jun 30 23:59:60 + S
+Leap 2015 Jun 30 23:59:60 + S
+Leap 2016 Dec 31 23:59:60 + S
+
+# UTC timestamp when this leap second list expires.
+# Any additional leap seconds will come after this.
+# This Expires line is commented out for now,
+# so that pre-2020a zic implementations do not reject this file.
+#Expires 2022 Dec 28 00:00:00
+
+# POSIX timestamps for the data in this file:
+#updated 1467936000 (2016-07-08 00:00:00 UTC)
+#expires 1672185600 (2022-12-28 00:00:00 UTC)
+
+# Updated through IERS Bulletin C63
+# File expires on: 28 December 2022
diff --git a/telegramer/include/pytz/zoneinfo/tzdata.zi b/telegramer/include/pytz/zoneinfo/tzdata.zi
new file mode 100644
index 0000000..e21fc92
--- /dev/null
+++ b/telegramer/include/pytz/zoneinfo/tzdata.zi
@@ -0,0 +1,4437 @@
+# version unknown-dirty
+# This zic input file is in the public domain.
+R d 1916 o - Jun 14 23s 1 S
+R d 1916 1919 - O Su>=1 23s 0 -
+R d 1917 o - Mar 24 23s 1 S
+R d 1918 o - Mar 9 23s 1 S
+R d 1919 o - Mar 1 23s 1 S
+R d 1920 o - F 14 23s 1 S
+R d 1920 o - O 23 23s 0 -
+R d 1921 o - Mar 14 23s 1 S
+R d 1921 o - Jun 21 23s 0 -
+R d 1939 o - S 11 23s 1 S
+R d 1939 o - N 19 1 0 -
+R d 1944 1945 - Ap M>=1 2 1 S
+R d 1944 o - O 8 2 0 -
+R d 1945 o - S 16 1 0 -
+R d 1971 o - Ap 25 23s 1 S
+R d 1971 o - S 26 23s 0 -
+R d 1977 o - May 6 0 1 S
+R d 1977 o - O 21 0 0 -
+R d 1978 o - Mar 24 1 1 S
+R d 1978 o - S 22 3 0 -
+R d 1980 o - Ap 25 0 1 S
+R d 1980 o - O 31 2 0 -
+Z Africa/Algiers 0:12:12 - LMT 1891 Mar 16
+0:9:21 - PMT 1911 Mar 11
+0 d WE%sT 1940 F 25 2
+1 d CE%sT 1946 O 7
+0 - WET 1956 Ja 29
+1 - CET 1963 Ap 14
+0 d WE%sT 1977 O 21
+1 d CE%sT 1979 O 26
+0 d WE%sT 1981 May
+1 - CET
+Z Atlantic/Cape_Verde -1:34:4 - LMT 1912 Ja 1 2u
+-2 - -02 1942 S
+-2 1 -01 1945 O 15
+-2 - -02 1975 N 25 2
+-1 - -01
+Z Africa/Ndjamena 1:0:12 - LMT 1912
+1 - WAT 1979 O 14
+1 1 WAST 1980 Mar 8
+1 - WAT
+Z Africa/Abidjan -0:16:8 - LMT 1912
+0 - GMT
+L Africa/Abidjan Africa/Accra
+L Africa/Abidjan Africa/Bamako
+L Africa/Abidjan Africa/Banjul
+L Africa/Abidjan Africa/Conakry
+L Africa/Abidjan Africa/Dakar
+L Africa/Abidjan Africa/Freetown
+L Africa/Abidjan Africa/Lome
+L Africa/Abidjan Africa/Nouakchott
+L Africa/Abidjan Africa/Ouagadougou
+L Africa/Abidjan Atlantic/St_Helena
+R K 1940 o - Jul 15 0 1 S
+R K 1940 o - O 1 0 0 -
+R K 1941 o - Ap 15 0 1 S
+R K 1941 o - S 16 0 0 -
+R K 1942 1944 - Ap 1 0 1 S
+R K 1942 o - O 27 0 0 -
+R K 1943 1945 - N 1 0 0 -
+R K 1945 o - Ap 16 0 1 S
+R K 1957 o - May 10 0 1 S
+R K 1957 1958 - O 1 0 0 -
+R K 1958 o - May 1 0 1 S
+R K 1959 1981 - May 1 1 1 S
+R K 1959 1965 - S 30 3 0 -
+R K 1966 1994 - O 1 3 0 -
+R K 1982 o - Jul 25 1 1 S
+R K 1983 o - Jul 12 1 1 S
+R K 1984 1988 - May 1 1 1 S
+R K 1989 o - May 6 1 1 S
+R K 1990 1994 - May 1 1 1 S
+R K 1995 2010 - Ap lastF 0s 1 S
+R K 1995 2005 - S lastTh 24 0 -
+R K 2006 o - S 21 24 0 -
+R K 2007 o - S Th>=1 24 0 -
+R K 2008 o - Au lastTh 24 0 -
+R K 2009 o - Au 20 24 0 -
+R K 2010 o - Au 10 24 0 -
+R K 2010 o - S 9 24 1 S
+R K 2010 o - S lastTh 24 0 -
+R K 2014 o - May 15 24 1 S
+R K 2014 o - Jun 26 24 0 -
+R K 2014 o - Jul 31 24 1 S
+R K 2014 o - S lastTh 24 0 -
+Z Africa/Cairo 2:5:9 - LMT 1900 O
+2 K EE%sT
+Z Africa/Bissau -1:2:20 - LMT 1912 Ja 1 1u
+-1 - -01 1975
+0 - GMT
+Z Africa/Nairobi 2:27:16 - LMT 1908 May
+2:30 - +0230 1928 Jun 30 24
+3 - EAT 1930 Ja 4 24
+2:30 - +0230 1936 D 31 24
+2:45 - +0245 1942 Jul 31 24
+3 - EAT
+L Africa/Nairobi Africa/Addis_Ababa
+L Africa/Nairobi Africa/Asmara
+L Africa/Nairobi Africa/Dar_es_Salaam
+L Africa/Nairobi Africa/Djibouti
+L Africa/Nairobi Africa/Kampala
+L Africa/Nairobi Africa/Mogadishu
+L Africa/Nairobi Indian/Antananarivo
+L Africa/Nairobi Indian/Comoro
+L Africa/Nairobi Indian/Mayotte
+Z Africa/Monrovia -0:43:8 - LMT 1882
+-0:43:8 - MMT 1919 Mar
+-0:44:30 - MMT 1972 Ja 7
+0 - GMT
+R L 1951 o - O 14 2 1 S
+R L 1952 o - Ja 1 0 0 -
+R L 1953 o - O 9 2 1 S
+R L 1954 o - Ja 1 0 0 -
+R L 1955 o - S 30 0 1 S
+R L 1956 o - Ja 1 0 0 -
+R L 1982 1984 - Ap 1 0 1 S
+R L 1982 1985 - O 1 0 0 -
+R L 1985 o - Ap 6 0 1 S
+R L 1986 o - Ap 4 0 1 S
+R L 1986 o - O 3 0 0 -
+R L 1987 1989 - Ap 1 0 1 S
+R L 1987 1989 - O 1 0 0 -
+R L 1997 o - Ap 4 0 1 S
+R L 1997 o - O 4 0 0 -
+R L 2013 o - Mar lastF 1 1 S
+R L 2013 o - O lastF 2 0 -
+Z Africa/Tripoli 0:52:44 - LMT 1920
+1 L CE%sT 1959
+2 - EET 1982
+1 L CE%sT 1990 May 4
+2 - EET 1996 S 30
+1 L CE%sT 1997 O 4
+2 - EET 2012 N 10 2
+1 L CE%sT 2013 O 25 2
+2 - EET
+R MU 1982 o - O 10 0 1 -
+R MU 1983 o - Mar 21 0 0 -
+R MU 2008 o - O lastSu 2 1 -
+R MU 2009 o - Mar lastSu 2 0 -
+Z Indian/Mauritius 3:50 - LMT 1907
+4 MU +04/+05
+R M 1939 o - S 12 0 1 -
+R M 1939 o - N 19 0 0 -
+R M 1940 o - F 25 0 1 -
+R M 1945 o - N 18 0 0 -
+R M 1950 o - Jun 11 0 1 -
+R M 1950 o - O 29 0 0 -
+R M 1967 o - Jun 3 12 1 -
+R M 1967 o - O 1 0 0 -
+R M 1974 o - Jun 24 0 1 -
+R M 1974 o - S 1 0 0 -
+R M 1976 1977 - May 1 0 1 -
+R M 1976 o - Au 1 0 0 -
+R M 1977 o - S 28 0 0 -
+R M 1978 o - Jun 1 0 1 -
+R M 1978 o - Au 4 0 0 -
+R M 2008 o - Jun 1 0 1 -
+R M 2008 o - S 1 0 0 -
+R M 2009 o - Jun 1 0 1 -
+R M 2009 o - Au 21 0 0 -
+R M 2010 o - May 2 0 1 -
+R M 2010 o - Au 8 0 0 -
+R M 2011 o - Ap 3 0 1 -
+R M 2011 o - Jul 31 0 0 -
+R M 2012 2013 - Ap lastSu 2 1 -
+R M 2012 o - Jul 20 3 0 -
+R M 2012 o - Au 20 2 1 -
+R M 2012 o - S 30 3 0 -
+R M 2013 o - Jul 7 3 0 -
+R M 2013 o - Au 10 2 1 -
+R M 2013 2018 - O lastSu 3 0 -
+R M 2014 2018 - Mar lastSu 2 1 -
+R M 2014 o - Jun 28 3 0 -
+R M 2014 o - Au 2 2 1 -
+R M 2015 o - Jun 14 3 0 -
+R M 2015 o - Jul 19 2 1 -
+R M 2016 o - Jun 5 3 0 -
+R M 2016 o - Jul 10 2 1 -
+R M 2017 o - May 21 3 0 -
+R M 2017 o - Jul 2 2 1 -
+R M 2018 o - May 13 3 0 -
+R M 2018 o - Jun 17 2 1 -
+R M 2019 o - May 5 3 -1 -
+R M 2019 o - Jun 9 2 0 -
+R M 2020 o - Ap 19 3 -1 -
+R M 2020 o - May 31 2 0 -
+R M 2021 o - Ap 11 3 -1 -
+R M 2021 o - May 16 2 0 -
+R M 2022 o - Mar 27 3 -1 -
+R M 2022 o - May 8 2 0 -
+R M 2023 o - Mar 19 3 -1 -
+R M 2023 o - Ap 30 2 0 -
+R M 2024 o - Mar 10 3 -1 -
+R M 2024 o - Ap 14 2 0 -
+R M 2025 o - F 23 3 -1 -
+R M 2025 o - Ap 6 2 0 -
+R M 2026 o - F 15 3 -1 -
+R M 2026 o - Mar 22 2 0 -
+R M 2027 o - F 7 3 -1 -
+R M 2027 o - Mar 14 2 0 -
+R M 2028 o - Ja 23 3 -1 -
+R M 2028 o - Mar 5 2 0 -
+R M 2029 o - Ja 14 3 -1 -
+R M 2029 o - F 18 2 0 -
+R M 2029 o - D 30 3 -1 -
+R M 2030 o - F 10 2 0 -
+R M 2030 o - D 22 3 -1 -
+R M 2031 o - F 2 2 0 -
+R M 2031 o - D 14 3 -1 -
+R M 2032 o - Ja 18 2 0 -
+R M 2032 o - N 28 3 -1 -
+R M 2033 o - Ja 9 2 0 -
+R M 2033 o - N 20 3 -1 -
+R M 2033 o - D 25 2 0 -
+R M 2034 o - N 5 3 -1 -
+R M 2034 o - D 17 2 0 -
+R M 2035 o - O 28 3 -1 -
+R M 2035 o - D 9 2 0 -
+R M 2036 o - O 19 3 -1 -
+R M 2036 o - N 23 2 0 -
+R M 2037 o - O 4 3 -1 -
+R M 2037 o - N 15 2 0 -
+R M 2038 o - S 26 3 -1 -
+R M 2038 o - N 7 2 0 -
+R M 2039 o - S 18 3 -1 -
+R M 2039 o - O 23 2 0 -
+R M 2040 o - S 2 3 -1 -
+R M 2040 o - O 14 2 0 -
+R M 2041 o - Au 25 3 -1 -
+R M 2041 o - S 29 2 0 -
+R M 2042 o - Au 10 3 -1 -
+R M 2042 o - S 21 2 0 -
+R M 2043 o - Au 2 3 -1 -
+R M 2043 o - S 13 2 0 -
+R M 2044 o - Jul 24 3 -1 -
+R M 2044 o - Au 28 2 0 -
+R M 2045 o - Jul 9 3 -1 -
+R M 2045 o - Au 20 2 0 -
+R M 2046 o - Jul 1 3 -1 -
+R M 2046 o - Au 12 2 0 -
+R M 2047 o - Jun 23 3 -1 -
+R M 2047 o - Jul 28 2 0 -
+R M 2048 o - Jun 7 3 -1 -
+R M 2048 o - Jul 19 2 0 -
+R M 2049 o - May 30 3 -1 -
+R M 2049 o - Jul 4 2 0 -
+R M 2050 o - May 15 3 -1 -
+R M 2050 o - Jun 26 2 0 -
+R M 2051 o - May 7 3 -1 -
+R M 2051 o - Jun 18 2 0 -
+R M 2052 o - Ap 28 3 -1 -
+R M 2052 o - Jun 2 2 0 -
+R M 2053 o - Ap 13 3 -1 -
+R M 2053 o - May 25 2 0 -
+R M 2054 o - Ap 5 3 -1 -
+R M 2054 o - May 17 2 0 -
+R M 2055 o - Mar 28 3 -1 -
+R M 2055 o - May 2 2 0 -
+R M 2056 o - Mar 12 3 -1 -
+R M 2056 o - Ap 23 2 0 -
+R M 2057 o - Mar 4 3 -1 -
+R M 2057 o - Ap 8 2 0 -
+R M 2058 o - F 17 3 -1 -
+R M 2058 o - Mar 31 2 0 -
+R M 2059 o - F 9 3 -1 -
+R M 2059 o - Mar 23 2 0 -
+R M 2060 o - F 1 3 -1 -
+R M 2060 o - Mar 7 2 0 -
+R M 2061 o - Ja 16 3 -1 -
+R M 2061 o - F 27 2 0 -
+R M 2062 o - Ja 8 3 -1 -
+R M 2062 o - F 19 2 0 -
+R M 2062 o - D 31 3 -1 -
+R M 2063 o - F 4 2 0 -
+R M 2063 o - D 16 3 -1 -
+R M 2064 o - Ja 27 2 0 -
+R M 2064 o - D 7 3 -1 -
+R M 2065 o - Ja 11 2 0 -
+R M 2065 o - N 22 3 -1 -
+R M 2066 o - Ja 3 2 0 -
+R M 2066 o - N 14 3 -1 -
+R M 2066 o - D 26 2 0 -
+R M 2067 o - N 6 3 -1 -
+R M 2067 o - D 11 2 0 -
+R M 2068 o - O 21 3 -1 -
+R M 2068 o - D 2 2 0 -
+R M 2069 o - O 13 3 -1 -
+R M 2069 o - N 24 2 0 -
+R M 2070 o - O 5 3 -1 -
+R M 2070 o - N 9 2 0 -
+R M 2071 o - S 20 3 -1 -
+R M 2071 o - N 1 2 0 -
+R M 2072 o - S 11 3 -1 -
+R M 2072 o - O 16 2 0 -
+R M 2073 o - Au 27 3 -1 -
+R M 2073 o - O 8 2 0 -
+R M 2074 o - Au 19 3 -1 -
+R M 2074 o - S 30 2 0 -
+R M 2075 o - Au 11 3 -1 -
+R M 2075 o - S 15 2 0 -
+R M 2076 o - Jul 26 3 -1 -
+R M 2076 o - S 6 2 0 -
+R M 2077 o - Jul 18 3 -1 -
+R M 2077 o - Au 29 2 0 -
+R M 2078 o - Jul 10 3 -1 -
+R M 2078 o - Au 14 2 0 -
+R M 2079 o - Jun 25 3 -1 -
+R M 2079 o - Au 6 2 0 -
+R M 2080 o - Jun 16 3 -1 -
+R M 2080 o - Jul 21 2 0 -
+R M 2081 o - Jun 1 3 -1 -
+R M 2081 o - Jul 13 2 0 -
+R M 2082 o - May 24 3 -1 -
+R M 2082 o - Jul 5 2 0 -
+R M 2083 o - May 16 3 -1 -
+R M 2083 o - Jun 20 2 0 -
+R M 2084 o - Ap 30 3 -1 -
+R M 2084 o - Jun 11 2 0 -
+R M 2085 o - Ap 22 3 -1 -
+R M 2085 o - Jun 3 2 0 -
+R M 2086 o - Ap 14 3 -1 -
+R M 2086 o - May 19 2 0 -
+R M 2087 o - Mar 30 3 -1 -
+R M 2087 o - May 11 2 0 -
+Z Africa/Casablanca -0:30:20 - LMT 1913 O 26
+0 M +00/+01 1984 Mar 16
+1 - +01 1986
+0 M +00/+01 2018 O 28 3
+1 M +01/+00
+Z Africa/El_Aaiun -0:52:48 - LMT 1934
+-1 - -01 1976 Ap 14
+0 M +00/+01 2018 O 28 3
+1 M +01/+00
+Z Africa/Maputo 2:10:20 - LMT 1903 Mar
+2 - CAT
+L Africa/Maputo Africa/Blantyre
+L Africa/Maputo Africa/Bujumbura
+L Africa/Maputo Africa/Gaborone
+L Africa/Maputo Africa/Harare
+L Africa/Maputo Africa/Kigali
+L Africa/Maputo Africa/Lubumbashi
+L Africa/Maputo Africa/Lusaka
+R NA 1994 o - Mar 21 0 -1 WAT
+R NA 1994 2017 - S Su>=1 2 0 CAT
+R NA 1995 2017 - Ap Su>=1 2 -1 WAT
+Z Africa/Windhoek 1:8:24 - LMT 1892 F 8
+1:30 - +0130 1903 Mar
+2 - SAST 1942 S 20 2
+2 1 SAST 1943 Mar 21 2
+2 - SAST 1990 Mar 21
+2 NA %s
+Z Africa/Lagos 0:13:35 - LMT 1905 Jul
+0 - GMT 1908 Jul
+0:13:35 - LMT 1914
+0:30 - +0030 1919 S
+1 - WAT
+L Africa/Lagos Africa/Bangui
+L Africa/Lagos Africa/Brazzaville
+L Africa/Lagos Africa/Douala
+L Africa/Lagos Africa/Kinshasa
+L Africa/Lagos Africa/Libreville
+L Africa/Lagos Africa/Luanda
+L Africa/Lagos Africa/Malabo
+L Africa/Lagos Africa/Niamey
+L Africa/Lagos Africa/Porto-Novo
+Z Indian/Reunion 3:41:52 - LMT 1911 Jun
+4 - +04
+Z Africa/Sao_Tome 0:26:56 - LMT 1884
+-0:36:45 - LMT 1912 Ja 1 0u
+0 - GMT 2018 Ja 1 1
+1 - WAT 2019 Ja 1 2
+0 - GMT
+Z Indian/Mahe 3:41:48 - LMT 1907
+4 - +04
+R SA 1942 1943 - S Su>=15 2 1 -
+R SA 1943 1944 - Mar Su>=15 2 0 -
+Z Africa/Johannesburg 1:52 - LMT 1892 F 8
+1:30 - SAST 1903 Mar
+2 SA SAST
+L Africa/Johannesburg Africa/Maseru
+L Africa/Johannesburg Africa/Mbabane
+R SD 1970 o - May 1 0 1 S
+R SD 1970 1985 - O 15 0 0 -
+R SD 1971 o - Ap 30 0 1 S
+R SD 1972 1985 - Ap lastSu 0 1 S
+Z Africa/Khartoum 2:10:8 - LMT 1931
+2 SD CA%sT 2000 Ja 15 12
+3 - EAT 2017 N
+2 - CAT
+Z Africa/Juba 2:6:28 - LMT 1931
+2 SD CA%sT 2000 Ja 15 12
+3 - EAT 2021 F
+2 - CAT
+R n 1939 o - Ap 15 23s 1 S
+R n 1939 o - N 18 23s 0 -
+R n 1940 o - F 25 23s 1 S
+R n 1941 o - O 6 0 0 -
+R n 1942 o - Mar 9 0 1 S
+R n 1942 o - N 2 3 0 -
+R n 1943 o - Mar 29 2 1 S
+R n 1943 o - Ap 17 2 0 -
+R n 1943 o - Ap 25 2 1 S
+R n 1943 o - O 4 2 0 -
+R n 1944 1945 - Ap M>=1 2 1 S
+R n 1944 o - O 8 0 0 -
+R n 1945 o - S 16 0 0 -
+R n 1977 o - Ap 30 0s 1 S
+R n 1977 o - S 24 0s 0 -
+R n 1978 o - May 1 0s 1 S
+R n 1978 o - O 1 0s 0 -
+R n 1988 o - Jun 1 0s 1 S
+R n 1988 1990 - S lastSu 0s 0 -
+R n 1989 o - Mar 26 0s 1 S
+R n 1990 o - May 1 0s 1 S
+R n 2005 o - May 1 0s 1 S
+R n 2005 o - S 30 1s 0 -
+R n 2006 2008 - Mar lastSu 2s 1 S
+R n 2006 2008 - O lastSu 2s 0 -
+Z Africa/Tunis 0:40:44 - LMT 1881 May 12
+0:9:21 - PMT 1911 Mar 11
+1 n CE%sT
+Z Antarctica/Casey 0 - -00 1969
+8 - +08 2009 O 18 2
+11 - +11 2010 Mar 5 2
+8 - +08 2011 O 28 2
+11 - +11 2012 F 21 17u
+8 - +08 2016 O 22
+11 - +11 2018 Mar 11 4
+8 - +08 2018 O 7 4
+11 - +11 2019 Mar 17 3
+8 - +08 2019 O 4 3
+11 - +11 2020 Mar 8 3
+8 - +08 2020 O 4 0:1
+11 - +11
+Z Antarctica/Davis 0 - -00 1957 Ja 13
+7 - +07 1964 N
+0 - -00 1969 F
+7 - +07 2009 O 18 2
+5 - +05 2010 Mar 10 20u
+7 - +07 2011 O 28 2
+5 - +05 2012 F 21 20u
+7 - +07
+Z Antarctica/Mawson 0 - -00 1954 F 13
+6 - +06 2009 O 18 2
+5 - +05
+Z Indian/Kerguelen 0 - -00 1950
+5 - +05
+R Tr 2005 ma - Mar lastSu 1u 2 +02
+R Tr 2004 ma - O lastSu 1u 0 +00
+Z Antarctica/Troll 0 - -00 2005 F 12
+0 Tr %s
+Z Antarctica/Vostok 0 - -00 1957 D 16
+6 - +06
+Z Antarctica/Rothera 0 - -00 1976 D
+-3 - -03
+Z Asia/Kabul 4:36:48 - LMT 1890
+4 - +04 1945
+4:30 - +0430
+R AM 2011 o - Mar lastSu 2s 1 -
+R AM 2011 o - O lastSu 2s 0 -
+Z Asia/Yerevan 2:58 - LMT 1924 May 2
+3 - +03 1957 Mar
+4 R +04/+05 1991 Mar 31 2s
+3 R +03/+04 1995 S 24 2s
+4 - +04 1997
+4 R +04/+05 2011
+4 AM +04/+05
+R AZ 1997 2015 - Mar lastSu 4 1 -
+R AZ 1997 2015 - O lastSu 5 0 -
+Z Asia/Baku 3:19:24 - LMT 1924 May 2
+3 - +03 1957 Mar
+4 R +04/+05 1991 Mar 31 2s
+3 R +03/+04 1992 S lastSu 2s
+4 - +04 1996
+4 E +04/+05 1997
+4 AZ +04/+05
+R BD 2009 o - Jun 19 23 1 -
+R BD 2009 o - D 31 24 0 -
+Z Asia/Dhaka 6:1:40 - LMT 1890
+5:53:20 - HMT 1941 O
+6:30 - +0630 1942 May 15
+5:30 - +0530 1942 S
+6:30 - +0630 1951 S 30
+6 - +06 2009
+6 BD +06/+07
+Z Asia/Thimphu 5:58:36 - LMT 1947 Au 15
+5:30 - +0530 1987 O
+6 - +06
+Z Indian/Chagos 4:49:40 - LMT 1907
+5 - +05 1996
+6 - +06
+Z Asia/Brunei 7:39:40 - LMT 1926 Mar
+7:30 - +0730 1933
+8 - +08
+Z Asia/Yangon 6:24:47 - LMT 1880
+6:24:47 - RMT 1920
+6:30 - +0630 1942 May
+9 - +09 1945 May 3
+6:30 - +0630
+R Sh 1919 o - Ap 12 24 1 D
+R Sh 1919 o - S 30 24 0 S
+R Sh 1940 o - Jun 1 0 1 D
+R Sh 1940 o - O 12 24 0 S
+R Sh 1941 o - Mar 15 0 1 D
+R Sh 1941 o - N 1 24 0 S
+R Sh 1942 o - Ja 31 0 1 D
+R Sh 1945 o - S 1 24 0 S
+R Sh 1946 o - May 15 0 1 D
+R Sh 1946 o - S 30 24 0 S
+R Sh 1947 o - Ap 15 0 1 D
+R Sh 1947 o - O 31 24 0 S
+R Sh 1948 1949 - May 1 0 1 D
+R Sh 1948 1949 - S 30 24 0 S
+R CN 1986 o - May 4 2 1 D
+R CN 1986 1991 - S Su>=11 2 0 S
+R CN 1987 1991 - Ap Su>=11 2 1 D
+Z Asia/Shanghai 8:5:43 - LMT 1901
+8 Sh C%sT 1949 May 28
+8 CN C%sT
+Z Asia/Urumqi 5:50:20 - LMT 1928
+6 - +06
+R HK 1946 o - Ap 21 0 1 S
+R HK 1946 o - D 1 3:30s 0 -
+R HK 1947 o - Ap 13 3:30s 1 S
+R HK 1947 o - N 30 3:30s 0 -
+R HK 1948 o - May 2 3:30s 1 S
+R HK 1948 1952 - O Su>=28 3:30s 0 -
+R HK 1949 1953 - Ap Su>=1 3:30 1 S
+R HK 1953 1964 - O Su>=31 3:30 0 -
+R HK 1954 1964 - Mar Su>=18 3:30 1 S
+R HK 1965 1976 - Ap Su>=16 3:30 1 S
+R HK 1965 1976 - O Su>=16 3:30 0 -
+R HK 1973 o - D 30 3:30 1 S
+R HK 1979 o - May 13 3:30 1 S
+R HK 1979 o - O 21 3:30 0 -
+Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 30 0:36:42
+8 - HKT 1941 Jun 15 3
+8 1 HKST 1941 O 1 4
+8 0:30 HKWT 1941 D 25
+9 - JST 1945 N 18 2
+8 HK HK%sT
+R f 1946 o - May 15 0 1 D
+R f 1946 o - O 1 0 0 S
+R f 1947 o - Ap 15 0 1 D
+R f 1947 o - N 1 0 0 S
+R f 1948 1951 - May 1 0 1 D
+R f 1948 1951 - O 1 0 0 S
+R f 1952 o - Mar 1 0 1 D
+R f 1952 1954 - N 1 0 0 S
+R f 1953 1959 - Ap 1 0 1 D
+R f 1955 1961 - O 1 0 0 S
+R f 1960 1961 - Jun 1 0 1 D
+R f 1974 1975 - Ap 1 0 1 D
+R f 1974 1975 - O 1 0 0 S
+R f 1979 o - Jul 1 0 1 D
+R f 1979 o - O 1 0 0 S
+Z Asia/Taipei 8:6 - LMT 1896
+8 - CST 1937 O
+9 - JST 1945 S 21 1
+8 f C%sT
+R _ 1942 1943 - Ap 30 23 1 -
+R _ 1942 o - N 17 23 0 -
+R _ 1943 o - S 30 23 0 S
+R _ 1946 o - Ap 30 23s 1 D
+R _ 1946 o - S 30 23s 0 S
+R _ 1947 o - Ap 19 23s 1 D
+R _ 1947 o - N 30 23s 0 S
+R _ 1948 o - May 2 23s 1 D
+R _ 1948 o - O 31 23s 0 S
+R _ 1949 1950 - Ap Sa>=1 23s 1 D
+R _ 1949 1950 - O lastSa 23s 0 S
+R _ 1951 o - Mar 31 23s 1 D
+R _ 1951 o - O 28 23s 0 S
+R _ 1952 1953 - Ap Sa>=1 23s 1 D
+R _ 1952 o - N 1 23s 0 S
+R _ 1953 1954 - O lastSa 23s 0 S
+R _ 1954 1956 - Mar Sa>=17 23s 1 D
+R _ 1955 o - N 5 23s 0 S
+R _ 1956 1964 - N Su>=1 3:30 0 S
+R _ 1957 1964 - Mar Su>=18 3:30 1 D
+R _ 1965 1973 - Ap Su>=16 3:30 1 D
+R _ 1965 1966 - O Su>=16 2:30 0 S
+R _ 1967 1976 - O Su>=16 3:30 0 S
+R _ 1973 o - D 30 3:30 1 D
+R _ 1975 1976 - Ap Su>=16 3:30 1 D
+R _ 1979 o - May 13 3:30 1 D
+R _ 1979 o - O Su>=16 3:30 0 S
+Z Asia/Macau 7:34:10 - LMT 1904 O 30
+8 - CST 1941 D 21 23
+9 _ +09/+10 1945 S 30 24
+8 _ C%sT
+R CY 1975 o - Ap 13 0 1 S
+R CY 1975 o - O 12 0 0 -
+R CY 1976 o - May 15 0 1 S
+R CY 1976 o - O 11 0 0 -
+R CY 1977 1980 - Ap Su>=1 0 1 S
+R CY 1977 o - S 25 0 0 -
+R CY 1978 o - O 2 0 0 -
+R CY 1979 1997 - S lastSu 0 0 -
+R CY 1981 1998 - Mar lastSu 0 1 S
+Z Asia/Nicosia 2:13:28 - LMT 1921 N 14
+2 CY EE%sT 1998 S
+2 E EE%sT
+Z Asia/Famagusta 2:15:48 - LMT 1921 N 14
+2 CY EE%sT 1998 S
+2 E EE%sT 2016 S 8
+3 - +03 2017 O 29 1u
+2 E EE%sT
+L Asia/Nicosia Europe/Nicosia
+Z Asia/Tbilisi 2:59:11 - LMT 1880
+2:59:11 - TBMT 1924 May 2
+3 - +03 1957 Mar
+4 R +04/+05 1991 Mar 31 2s
+3 R +03/+04 1992
+3 e +03/+04 1994 S lastSu
+4 e +04/+05 1996 O lastSu
+4 1 +05 1997 Mar lastSu
+4 e +04/+05 2004 Jun 27
+3 R +03/+04 2005 Mar lastSu 2
+4 - +04
+Z Asia/Dili 8:22:20 - LMT 1912
+8 - +08 1942 F 21 23
+9 - +09 1976 May 3
+8 - +08 2000 S 17
+9 - +09
+Z Asia/Kolkata 5:53:28 - LMT 1854 Jun 28
+5:53:20 - HMT 1870
+5:21:10 - MMT 1906
+5:30 - IST 1941 O
+5:30 1 +0630 1942 May 15
+5:30 - IST 1942 S
+5:30 1 +0630 1945 O 15
+5:30 - IST
+Z Asia/Jakarta 7:7:12 - LMT 1867 Au 10
+7:7:12 - BMT 1923 D 31 23:47:12
+7:20 - +0720 1932 N
+7:30 - +0730 1942 Mar 23
+9 - +09 1945 S 23
+7:30 - +0730 1948 May
+8 - +08 1950 May
+7:30 - +0730 1964
+7 - WIB
+Z Asia/Pontianak 7:17:20 - LMT 1908 May
+7:17:20 - PMT 1932 N
+7:30 - +0730 1942 Ja 29
+9 - +09 1945 S 23
+7:30 - +0730 1948 May
+8 - +08 1950 May
+7:30 - +0730 1964
+8 - WITA 1988
+7 - WIB
+Z Asia/Makassar 7:57:36 - LMT 1920
+7:57:36 - MMT 1932 N
+8 - +08 1942 F 9
+9 - +09 1945 S 23
+8 - WITA
+Z Asia/Jayapura 9:22:48 - LMT 1932 N
+9 - +09 1944 S
+9:30 - +0930 1964
+9 - WIT
+R i 1978 1980 - Mar 20 24 1 -
+R i 1978 o - O 20 24 0 -
+R i 1979 o - S 18 24 0 -
+R i 1980 o - S 22 24 0 -
+R i 1991 o - May 2 24 1 -
+R i 1992 1995 - Mar 21 24 1 -
+R i 1991 1995 - S 21 24 0 -
+R i 1996 o - Mar 20 24 1 -
+R i 1996 o - S 20 24 0 -
+R i 1997 1999 - Mar 21 24 1 -
+R i 1997 1999 - S 21 24 0 -
+R i 2000 o - Mar 20 24 1 -
+R i 2000 o - S 20 24 0 -
+R i 2001 2003 - Mar 21 24 1 -
+R i 2001 2003 - S 21 24 0 -
+R i 2004 o - Mar 20 24 1 -
+R i 2004 o - S 20 24 0 -
+R i 2005 o - Mar 21 24 1 -
+R i 2005 o - S 21 24 0 -
+R i 2008 o - Mar 20 24 1 -
+R i 2008 o - S 20 24 0 -
+R i 2009 2011 - Mar 21 24 1 -
+R i 2009 2011 - S 21 24 0 -
+R i 2012 o - Mar 20 24 1 -
+R i 2012 o - S 20 24 0 -
+R i 2013 2015 - Mar 21 24 1 -
+R i 2013 2015 - S 21 24 0 -
+R i 2016 o - Mar 20 24 1 -
+R i 2016 o - S 20 24 0 -
+R i 2017 2019 - Mar 21 24 1 -
+R i 2017 2019 - S 21 24 0 -
+R i 2020 o - Mar 20 24 1 -
+R i 2020 o - S 20 24 0 -
+R i 2021 2023 - Mar 21 24 1 -
+R i 2021 2023 - S 21 24 0 -
+R i 2024 o - Mar 20 24 1 -
+R i 2024 o - S 20 24 0 -
+R i 2025 2027 - Mar 21 24 1 -
+R i 2025 2027 - S 21 24 0 -
+R i 2028 2029 - Mar 20 24 1 -
+R i 2028 2029 - S 20 24 0 -
+R i 2030 2031 - Mar 21 24 1 -
+R i 2030 2031 - S 21 24 0 -
+R i 2032 2033 - Mar 20 24 1 -
+R i 2032 2033 - S 20 24 0 -
+R i 2034 2035 - Mar 21 24 1 -
+R i 2034 2035 - S 21 24 0 -
+R i 2036 2037 - Mar 20 24 1 -
+R i 2036 2037 - S 20 24 0 -
+R i 2038 2039 - Mar 21 24 1 -
+R i 2038 2039 - S 21 24 0 -
+R i 2040 2041 - Mar 20 24 1 -
+R i 2040 2041 - S 20 24 0 -
+R i 2042 2043 - Mar 21 24 1 -
+R i 2042 2043 - S 21 24 0 -
+R i 2044 2045 - Mar 20 24 1 -
+R i 2044 2045 - S 20 24 0 -
+R i 2046 2047 - Mar 21 24 1 -
+R i 2046 2047 - S 21 24 0 -
+R i 2048 2049 - Mar 20 24 1 -
+R i 2048 2049 - S 20 24 0 -
+R i 2050 2051 - Mar 21 24 1 -
+R i 2050 2051 - S 21 24 0 -
+R i 2052 2053 - Mar 20 24 1 -
+R i 2052 2053 - S 20 24 0 -
+R i 2054 2055 - Mar 21 24 1 -
+R i 2054 2055 - S 21 24 0 -
+R i 2056 2057 - Mar 20 24 1 -
+R i 2056 2057 - S 20 24 0 -
+R i 2058 2059 - Mar 21 24 1 -
+R i 2058 2059 - S 21 24 0 -
+R i 2060 2062 - Mar 20 24 1 -
+R i 2060 2062 - S 20 24 0 -
+R i 2063 o - Mar 21 24 1 -
+R i 2063 o - S 21 24 0 -
+R i 2064 2066 - Mar 20 24 1 -
+R i 2064 2066 - S 20 24 0 -
+R i 2067 o - Mar 21 24 1 -
+R i 2067 o - S 21 24 0 -
+R i 2068 2070 - Mar 20 24 1 -
+R i 2068 2070 - S 20 24 0 -
+R i 2071 o - Mar 21 24 1 -
+R i 2071 o - S 21 24 0 -
+R i 2072 2074 - Mar 20 24 1 -
+R i 2072 2074 - S 20 24 0 -
+R i 2075 o - Mar 21 24 1 -
+R i 2075 o - S 21 24 0 -
+R i 2076 2078 - Mar 20 24 1 -
+R i 2076 2078 - S 20 24 0 -
+R i 2079 o - Mar 21 24 1 -
+R i 2079 o - S 21 24 0 -
+R i 2080 2082 - Mar 20 24 1 -
+R i 2080 2082 - S 20 24 0 -
+R i 2083 o - Mar 21 24 1 -
+R i 2083 o - S 21 24 0 -
+R i 2084 2086 - Mar 20 24 1 -
+R i 2084 2086 - S 20 24 0 -
+R i 2087 o - Mar 21 24 1 -
+R i 2087 o - S 21 24 0 -
+R i 2088 ma - Mar 20 24 1 -
+R i 2088 ma - S 20 24 0 -
+Z Asia/Tehran 3:25:44 - LMT 1916
+3:25:44 - TMT 1946
+3:30 - +0330 1977 N
+4 i +04/+05 1979
+3:30 i +0330/+0430
+R IQ 1982 o - May 1 0 1 -
+R IQ 1982 1984 - O 1 0 0 -
+R IQ 1983 o - Mar 31 0 1 -
+R IQ 1984 1985 - Ap 1 0 1 -
+R IQ 1985 1990 - S lastSu 1s 0 -
+R IQ 1986 1990 - Mar lastSu 1s 1 -
+R IQ 1991 2007 - Ap 1 3s 1 -
+R IQ 1991 2007 - O 1 3s 0 -
+Z Asia/Baghdad 2:57:40 - LMT 1890
+2:57:36 - BMT 1918
+3 - +03 1982 May
+3 IQ +03/+04
+R Z 1940 o - May 31 24u 1 D
+R Z 1940 o - S 30 24u 0 S
+R Z 1940 o - N 16 24u 1 D
+R Z 1942 1946 - O 31 24u 0 S
+R Z 1943 1944 - Mar 31 24u 1 D
+R Z 1945 1946 - Ap 15 24u 1 D
+R Z 1948 o - May 22 24u 2 DD
+R Z 1948 o - Au 31 24u 1 D
+R Z 1948 1949 - O 31 24u 0 S
+R Z 1949 o - Ap 30 24u 1 D
+R Z 1950 o - Ap 15 24u 1 D
+R Z 1950 o - S 14 24u 0 S
+R Z 1951 o - Mar 31 24u 1 D
+R Z 1951 o - N 10 24u 0 S
+R Z 1952 o - Ap 19 24u 1 D
+R Z 1952 o - O 18 24u 0 S
+R Z 1953 o - Ap 11 24u 1 D
+R Z 1953 o - S 12 24u 0 S
+R Z 1954 o - Jun 12 24u 1 D
+R Z 1954 o - S 11 24u 0 S
+R Z 1955 o - Jun 11 24u 1 D
+R Z 1955 o - S 10 24u 0 S
+R Z 1956 o - Jun 2 24u 1 D
+R Z 1956 o - S 29 24u 0 S
+R Z 1957 o - Ap 27 24u 1 D
+R Z 1957 o - S 21 24u 0 S
+R Z 1974 o - Jul 6 24 1 D
+R Z 1974 o - O 12 24 0 S
+R Z 1975 o - Ap 19 24 1 D
+R Z 1975 o - Au 30 24 0 S
+R Z 1980 o - Au 2 24s 1 D
+R Z 1980 o - S 13 24s 0 S
+R Z 1984 o - May 5 24s 1 D
+R Z 1984 o - Au 25 24s 0 S
+R Z 1985 o - Ap 13 24 1 D
+R Z 1985 o - Au 31 24 0 S
+R Z 1986 o - May 17 24 1 D
+R Z 1986 o - S 6 24 0 S
+R Z 1987 o - Ap 14 24 1 D
+R Z 1987 o - S 12 24 0 S
+R Z 1988 o - Ap 9 24 1 D
+R Z 1988 o - S 3 24 0 S
+R Z 1989 o - Ap 29 24 1 D
+R Z 1989 o - S 2 24 0 S
+R Z 1990 o - Mar 24 24 1 D
+R Z 1990 o - Au 25 24 0 S
+R Z 1991 o - Mar 23 24 1 D
+R Z 1991 o - Au 31 24 0 S
+R Z 1992 o - Mar 28 24 1 D
+R Z 1992 o - S 5 24 0 S
+R Z 1993 o - Ap 2 0 1 D
+R Z 1993 o - S 5 0 0 S
+R Z 1994 o - Ap 1 0 1 D
+R Z 1994 o - Au 28 0 0 S
+R Z 1995 o - Mar 31 0 1 D
+R Z 1995 o - S 3 0 0 S
+R Z 1996 o - Mar 14 24 1 D
+R Z 1996 o - S 15 24 0 S
+R Z 1997 o - Mar 20 24 1 D
+R Z 1997 o - S 13 24 0 S
+R Z 1998 o - Mar 20 0 1 D
+R Z 1998 o - S 6 0 0 S
+R Z 1999 o - Ap 2 2 1 D
+R Z 1999 o - S 3 2 0 S
+R Z 2000 o - Ap 14 2 1 D
+R Z 2000 o - O 6 1 0 S
+R Z 2001 o - Ap 9 1 1 D
+R Z 2001 o - S 24 1 0 S
+R Z 2002 o - Mar 29 1 1 D
+R Z 2002 o - O 7 1 0 S
+R Z 2003 o - Mar 28 1 1 D
+R Z 2003 o - O 3 1 0 S
+R Z 2004 o - Ap 7 1 1 D
+R Z 2004 o - S 22 1 0 S
+R Z 2005 2012 - Ap F<=1 2 1 D
+R Z 2005 o - O 9 2 0 S
+R Z 2006 o - O 1 2 0 S
+R Z 2007 o - S 16 2 0 S
+R Z 2008 o - O 5 2 0 S
+R Z 2009 o - S 27 2 0 S
+R Z 2010 o - S 12 2 0 S
+R Z 2011 o - O 2 2 0 S
+R Z 2012 o - S 23 2 0 S
+R Z 2013 ma - Mar F>=23 2 1 D
+R Z 2013 ma - O lastSu 2 0 S
+Z Asia/Jerusalem 2:20:54 - LMT 1880
+2:20:40 - JMT 1918
+2 Z I%sT
+R JP 1948 o - May Sa>=1 24 1 D
+R JP 1948 1951 - S Sa>=8 25 0 S
+R JP 1949 o - Ap Sa>=1 24 1 D
+R JP 1950 1951 - May Sa>=1 24 1 D
+Z Asia/Tokyo 9:18:59 - LMT 1887 D 31 15u
+9 JP J%sT
+R J 1973 o - Jun 6 0 1 S
+R J 1973 1975 - O 1 0 0 -
+R J 1974 1977 - May 1 0 1 S
+R J 1976 o - N 1 0 0 -
+R J 1977 o - O 1 0 0 -
+R J 1978 o - Ap 30 0 1 S
+R J 1978 o - S 30 0 0 -
+R J 1985 o - Ap 1 0 1 S
+R J 1985 o - O 1 0 0 -
+R J 1986 1988 - Ap F>=1 0 1 S
+R J 1986 1990 - O F>=1 0 0 -
+R J 1989 o - May 8 0 1 S
+R J 1990 o - Ap 27 0 1 S
+R J 1991 o - Ap 17 0 1 S
+R J 1991 o - S 27 0 0 -
+R J 1992 o - Ap 10 0 1 S
+R J 1992 1993 - O F>=1 0 0 -
+R J 1993 1998 - Ap F>=1 0 1 S
+R J 1994 o - S F>=15 0 0 -
+R J 1995 1998 - S F>=15 0s 0 -
+R J 1999 o - Jul 1 0s 1 S
+R J 1999 2002 - S lastF 0s 0 -
+R J 2000 2001 - Mar lastTh 0s 1 S
+R J 2002 2012 - Mar lastTh 24 1 S
+R J 2003 o - O 24 0s 0 -
+R J 2004 o - O 15 0s 0 -
+R J 2005 o - S lastF 0s 0 -
+R J 2006 2011 - O lastF 0s 0 -
+R J 2013 o - D 20 0 0 -
+R J 2014 2021 - Mar lastTh 24 1 S
+R J 2014 ma - O lastF 0s 0 -
+R J 2022 ma - F lastTh 24 1 S
+Z Asia/Amman 2:23:44 - LMT 1931
+2 J EE%sT
+Z Asia/Almaty 5:7:48 - LMT 1924 May 2
+5 - +05 1930 Jun 21
+6 R +06/+07 1991 Mar 31 2s
+5 R +05/+06 1992 Ja 19 2s
+6 R +06/+07 2004 O 31 2s
+6 - +06
+Z Asia/Qyzylorda 4:21:52 - LMT 1924 May 2
+4 - +04 1930 Jun 21
+5 - +05 1981 Ap
+5 1 +06 1981 O
+6 - +06 1982 Ap
+5 R +05/+06 1991 Mar 31 2s
+4 R +04/+05 1991 S 29 2s
+5 R +05/+06 1992 Ja 19 2s
+6 R +06/+07 1992 Mar 29 2s
+5 R +05/+06 2004 O 31 2s
+6 - +06 2018 D 21
+5 - +05
+Z Asia/Qostanay 4:14:28 - LMT 1924 May 2
+4 - +04 1930 Jun 21
+5 - +05 1981 Ap
+5 1 +06 1981 O
+6 - +06 1982 Ap
+5 R +05/+06 1991 Mar 31 2s
+4 R +04/+05 1992 Ja 19 2s
+5 R +05/+06 2004 O 31 2s
+6 - +06
+Z Asia/Aqtobe 3:48:40 - LMT 1924 May 2
+4 - +04 1930 Jun 21
+5 - +05 1981 Ap
+5 1 +06 1981 O
+6 - +06 1982 Ap
+5 R +05/+06 1991 Mar 31 2s
+4 R +04/+05 1992 Ja 19 2s
+5 R +05/+06 2004 O 31 2s
+5 - +05
+Z Asia/Aqtau 3:21:4 - LMT 1924 May 2
+4 - +04 1930 Jun 21
+5 - +05 1981 O
+6 - +06 1982 Ap
+5 R +05/+06 1991 Mar 31 2s
+4 R +04/+05 1992 Ja 19 2s
+5 R +05/+06 1994 S 25 2s
+4 R +04/+05 2004 O 31 2s
+5 - +05
+Z Asia/Atyrau 3:27:44 - LMT 1924 May 2
+3 - +03 1930 Jun 21
+5 - +05 1981 O
+6 - +06 1982 Ap
+5 R +05/+06 1991 Mar 31 2s
+4 R +04/+05 1992 Ja 19 2s
+5 R +05/+06 1999 Mar 28 2s
+4 R +04/+05 2004 O 31 2s
+5 - +05
+Z Asia/Oral 3:25:24 - LMT 1924 May 2
+3 - +03 1930 Jun 21
+5 - +05 1981 Ap
+5 1 +06 1981 O
+6 - +06 1982 Ap
+5 R +05/+06 1989 Mar 26 2s
+4 R +04/+05 1992 Ja 19 2s
+5 R +05/+06 1992 Mar 29 2s
+4 R +04/+05 2004 O 31 2s
+5 - +05
+R KG 1992 1996 - Ap Su>=7 0s 1 -
+R KG 1992 1996 - S lastSu 0 0 -
+R KG 1997 2005 - Mar lastSu 2:30 1 -
+R KG 1997 2004 - O lastSu 2:30 0 -
+Z Asia/Bishkek 4:58:24 - LMT 1924 May 2
+5 - +05 1930 Jun 21
+6 R +06/+07 1991 Mar 31 2s
+5 R +05/+06 1991 Au 31 2
+5 KG +05/+06 2005 Au 12
+6 - +06
+R KR 1948 o - Jun 1 0 1 D
+R KR 1948 o - S 12 24 0 S
+R KR 1949 o - Ap 3 0 1 D
+R KR 1949 1951 - S Sa>=7 24 0 S
+R KR 1950 o - Ap 1 0 1 D
+R KR 1951 o - May 6 0 1 D
+R KR 1955 o - May 5 0 1 D
+R KR 1955 o - S 8 24 0 S
+R KR 1956 o - May 20 0 1 D
+R KR 1956 o - S 29 24 0 S
+R KR 1957 1960 - May Su>=1 0 1 D
+R KR 1957 1960 - S Sa>=17 24 0 S
+R KR 1987 1988 - May Su>=8 2 1 D
+R KR 1987 1988 - O Su>=8 3 0 S
+Z Asia/Seoul 8:27:52 - LMT 1908 Ap
+8:30 - KST 1912
+9 - JST 1945 S 8
+9 KR K%sT 1954 Mar 21
+8:30 KR K%sT 1961 Au 10
+9 KR K%sT
+Z Asia/Pyongyang 8:23 - LMT 1908 Ap
+8:30 - KST 1912
+9 - JST 1945 Au 24
+9 - KST 2015 Au 15
+8:30 - KST 2018 May 4 23:30
+9 - KST
+R l 1920 o - Mar 28 0 1 S
+R l 1920 o - O 25 0 0 -
+R l 1921 o - Ap 3 0 1 S
+R l 1921 o - O 3 0 0 -
+R l 1922 o - Mar 26 0 1 S
+R l 1922 o - O 8 0 0 -
+R l 1923 o - Ap 22 0 1 S
+R l 1923 o - S 16 0 0 -
+R l 1957 1961 - May 1 0 1 S
+R l 1957 1961 - O 1 0 0 -
+R l 1972 o - Jun 22 0 1 S
+R l 1972 1977 - O 1 0 0 -
+R l 1973 1977 - May 1 0 1 S
+R l 1978 o - Ap 30 0 1 S
+R l 1978 o - S 30 0 0 -
+R l 1984 1987 - May 1 0 1 S
+R l 1984 1991 - O 16 0 0 -
+R l 1988 o - Jun 1 0 1 S
+R l 1989 o - May 10 0 1 S
+R l 1990 1992 - May 1 0 1 S
+R l 1992 o - O 4 0 0 -
+R l 1993 ma - Mar lastSu 0 1 S
+R l 1993 1998 - S lastSu 0 0 -
+R l 1999 ma - O lastSu 0 0 -
+Z Asia/Beirut 2:22 - LMT 1880
+2 l EE%sT
+R NB 1935 1941 - S 14 0 0:20 -
+R NB 1935 1941 - D 14 0 0 -
+Z Asia/Kuala_Lumpur 6:46:46 - LMT 1901
+6:55:25 - SMT 1905 Jun
+7 - +07 1933
+7 0:20 +0720 1936
+7:20 - +0720 1941 S
+7:30 - +0730 1942 F 16
+9 - +09 1945 S 12
+7:30 - +0730 1982
+8 - +08
+Z Asia/Kuching 7:21:20 - LMT 1926 Mar
+7:30 - +0730 1933
+8 NB +08/+0820 1942 F 16
+9 - +09 1945 S 12
+8 - +08
+Z Indian/Maldives 4:54 - LMT 1880
+4:54 - MMT 1960
+5 - +05
+R X 1983 1984 - Ap 1 0 1 -
+R X 1983 o - O 1 0 0 -
+R X 1985 1998 - Mar lastSu 0 1 -
+R X 1984 1998 - S lastSu 0 0 -
+R X 2001 o - Ap lastSa 2 1 -
+R X 2001 2006 - S lastSa 2 0 -
+R X 2002 2006 - Mar lastSa 2 1 -
+R X 2015 2016 - Mar lastSa 2 1 -
+R X 2015 2016 - S lastSa 0 0 -
+Z Asia/Hovd 6:6:36 - LMT 1905 Au
+6 - +06 1978
+7 X +07/+08
+Z Asia/Ulaanbaatar 7:7:32 - LMT 1905 Au
+7 - +07 1978
+8 X +08/+09
+Z Asia/Choibalsan 7:38 - LMT 1905 Au
+7 - +07 1978
+8 - +08 1983 Ap
+9 X +09/+10 2008 Mar 31
+8 X +08/+09
+Z Asia/Kathmandu 5:41:16 - LMT 1920
+5:30 - +0530 1986
+5:45 - +0545
+R PK 2002 o - Ap Su>=2 0 1 S
+R PK 2002 o - O Su>=2 0 0 -
+R PK 2008 o - Jun 1 0 1 S
+R PK 2008 2009 - N 1 0 0 -
+R PK 2009 o - Ap 15 0 1 S
+Z Asia/Karachi 4:28:12 - LMT 1907
+5:30 - +0530 1942 S
+5:30 1 +0630 1945 O 15
+5:30 - +0530 1951 S 30
+5 - +05 1971 Mar 26
+5 PK PK%sT
+R P 1999 2005 - Ap F>=15 0 1 S
+R P 1999 2003 - O F>=15 0 0 -
+R P 2004 o - O 1 1 0 -
+R P 2005 o - O 4 2 0 -
+R P 2006 2007 - Ap 1 0 1 S
+R P 2006 o - S 22 0 0 -
+R P 2007 o - S 13 2 0 -
+R P 2008 2009 - Mar lastF 0 1 S
+R P 2008 o - S 1 0 0 -
+R P 2009 o - S 4 1 0 -
+R P 2010 o - Mar 26 0 1 S
+R P 2010 o - Au 11 0 0 -
+R P 2011 o - Ap 1 0:1 1 S
+R P 2011 o - Au 1 0 0 -
+R P 2011 o - Au 30 0 1 S
+R P 2011 o - S 30 0 0 -
+R P 2012 2014 - Mar lastTh 24 1 S
+R P 2012 o - S 21 1 0 -
+R P 2013 o - S 27 0 0 -
+R P 2014 o - O 24 0 0 -
+R P 2015 o - Mar 28 0 1 S
+R P 2015 o - O 23 1 0 -
+R P 2016 2018 - Mar Sa>=24 1 1 S
+R P 2016 2018 - O Sa>=24 1 0 -
+R P 2019 o - Mar 29 0 1 S
+R P 2019 o - O Sa>=24 0 0 -
+R P 2020 2021 - Mar Sa>=24 0 1 S
+R P 2020 o - O 24 1 0 -
+R P 2021 ma - O F>=23 1 0 -
+R P 2022 ma - Mar Su>=25 0 1 S
+Z Asia/Gaza 2:17:52 - LMT 1900 O
+2 Z EET/EEST 1948 May 15
+2 K EE%sT 1967 Jun 5
+2 Z I%sT 1996
+2 J EE%sT 1999
+2 P EE%sT 2008 Au 29
+2 - EET 2008 S
+2 P EE%sT 2010
+2 - EET 2010 Mar 27 0:1
+2 P EE%sT 2011 Au
+2 - EET 2012
+2 P EE%sT
+Z Asia/Hebron 2:20:23 - LMT 1900 O
+2 Z EET/EEST 1948 May 15
+2 K EE%sT 1967 Jun 5
+2 Z I%sT 1996
+2 J EE%sT 1999
+2 P EE%sT
+R PH 1936 o - N 1 0 1 D
+R PH 1937 o - F 1 0 0 S
+R PH 1954 o - Ap 12 0 1 D
+R PH 1954 o - Jul 1 0 0 S
+R PH 1978 o - Mar 22 0 1 D
+R PH 1978 o - S 21 0 0 S
+Z Asia/Manila -15:56 - LMT 1844 D 31
+8:4 - LMT 1899 May 11
+8 PH P%sT 1942 May
+9 - JST 1944 N
+8 PH P%sT
+Z Asia/Qatar 3:26:8 - LMT 1920
+4 - +04 1972 Jun
+3 - +03
+L Asia/Qatar Asia/Bahrain
+Z Asia/Riyadh 3:6:52 - LMT 1947 Mar 14
+3 - +03
+L Asia/Riyadh Antarctica/Syowa
+L Asia/Riyadh Asia/Aden
+L Asia/Riyadh Asia/Kuwait
+Z Asia/Singapore 6:55:25 - LMT 1901
+6:55:25 - SMT 1905 Jun
+7 - +07 1933
+7 0:20 +0720 1936
+7:20 - +0720 1941 S
+7:30 - +0730 1942 F 16
+9 - +09 1945 S 12
+7:30 - +0730 1982
+8 - +08
+Z Asia/Colombo 5:19:24 - LMT 1880
+5:19:32 - MMT 1906
+5:30 - +0530 1942 Ja 5
+5:30 0:30 +06 1942 S
+5:30 1 +0630 1945 O 16 2
+5:30 - +0530 1996 May 25
+6:30 - +0630 1996 O 26 0:30
+6 - +06 2006 Ap 15 0:30
+5:30 - +0530
+R S 1920 1923 - Ap Su>=15 2 1 S
+R S 1920 1923 - O Su>=1 2 0 -
+R S 1962 o - Ap 29 2 1 S
+R S 1962 o - O 1 2 0 -
+R S 1963 1965 - May 1 2 1 S
+R S 1963 o - S 30 2 0 -
+R S 1964 o - O 1 2 0 -
+R S 1965 o - S 30 2 0 -
+R S 1966 o - Ap 24 2 1 S
+R S 1966 1976 - O 1 2 0 -
+R S 1967 1978 - May 1 2 1 S
+R S 1977 1978 - S 1 2 0 -
+R S 1983 1984 - Ap 9 2 1 S
+R S 1983 1984 - O 1 2 0 -
+R S 1986 o - F 16 2 1 S
+R S 1986 o - O 9 2 0 -
+R S 1987 o - Mar 1 2 1 S
+R S 1987 1988 - O 31 2 0 -
+R S 1988 o - Mar 15 2 1 S
+R S 1989 o - Mar 31 2 1 S
+R S 1989 o - O 1 2 0 -
+R S 1990 o - Ap 1 2 1 S
+R S 1990 o - S 30 2 0 -
+R S 1991 o - Ap 1 0 1 S
+R S 1991 1992 - O 1 0 0 -
+R S 1992 o - Ap 8 0 1 S
+R S 1993 o - Mar 26 0 1 S
+R S 1993 o - S 25 0 0 -
+R S 1994 1996 - Ap 1 0 1 S
+R S 1994 2005 - O 1 0 0 -
+R S 1997 1998 - Mar lastM 0 1 S
+R S 1999 2006 - Ap 1 0 1 S
+R S 2006 o - S 22 0 0 -
+R S 2007 o - Mar lastF 0 1 S
+R S 2007 o - N F>=1 0 0 -
+R S 2008 o - Ap F>=1 0 1 S
+R S 2008 o - N 1 0 0 -
+R S 2009 o - Mar lastF 0 1 S
+R S 2010 2011 - Ap F>=1 0 1 S
+R S 2012 ma - Mar lastF 0 1 S
+R S 2009 ma - O lastF 0 0 -
+Z Asia/Damascus 2:25:12 - LMT 1920
+2 S EE%sT
+Z Asia/Dushanbe 4:35:12 - LMT 1924 May 2
+5 - +05 1930 Jun 21
+6 R +06/+07 1991 Mar 31 2s
+5 1 +05/+06 1991 S 9 2s
+5 - +05
+Z Asia/Bangkok 6:42:4 - LMT 1880
+6:42:4 - BMT 1920 Ap
+7 - +07
+L Asia/Bangkok Asia/Phnom_Penh
+L Asia/Bangkok Asia/Vientiane
+Z Asia/Ashgabat 3:53:32 - LMT 1924 May 2
+4 - +04 1930 Jun 21
+5 R +05/+06 1991 Mar 31 2
+4 R +04/+05 1992 Ja 19 2
+5 - +05
+Z Asia/Dubai 3:41:12 - LMT 1920
+4 - +04
+L Asia/Dubai Asia/Muscat
+Z Asia/Samarkand 4:27:53 - LMT 1924 May 2
+4 - +04 1930 Jun 21
+5 - +05 1981 Ap
+5 1 +06 1981 O
+6 - +06 1982 Ap
+5 R +05/+06 1992
+5 - +05
+Z Asia/Tashkent 4:37:11 - LMT 1924 May 2
+5 - +05 1930 Jun 21
+6 R +06/+07 1991 Mar 31 2
+5 R +05/+06 1992
+5 - +05
+Z Asia/Ho_Chi_Minh 7:6:40 - LMT 1906 Jul
+7:6:30 - PLMT 1911 May
+7 - +07 1942 D 31 23
+8 - +08 1945 Mar 14 23
+9 - +09 1945 S 2
+7 - +07 1947 Ap
+8 - +08 1955 Jul
+7 - +07 1959 D 31 23
+8 - +08 1975 Jun 13
+7 - +07
+R AU 1917 o - Ja 1 2s 1 D
+R AU 1917 o - Mar lastSu 2s 0 S
+R AU 1942 o - Ja 1 2s 1 D
+R AU 1942 o - Mar lastSu 2s 0 S
+R AU 1942 o - S 27 2s 1 D
+R AU 1943 1944 - Mar lastSu 2s 0 S
+R AU 1943 o - O 3 2s 1 D
+Z Australia/Darwin 8:43:20 - LMT 1895 F
+9 - ACST 1899 May
+9:30 AU AC%sT
+R AW 1974 o - O lastSu 2s 1 D
+R AW 1975 o - Mar Su>=1 2s 0 S
+R AW 1983 o - O lastSu 2s 1 D
+R AW 1984 o - Mar Su>=1 2s 0 S
+R AW 1991 o - N 17 2s 1 D
+R AW 1992 o - Mar Su>=1 2s 0 S
+R AW 2006 o - D 3 2s 1 D
+R AW 2007 2009 - Mar lastSu 2s 0 S
+R AW 2007 2008 - O lastSu 2s 1 D
+Z Australia/Perth 7:43:24 - LMT 1895 D
+8 AU AW%sT 1943 Jul
+8 AW AW%sT
+Z Australia/Eucla 8:35:28 - LMT 1895 D
+8:45 AU +0845/+0945 1943 Jul
+8:45 AW +0845/+0945
+R AQ 1971 o - O lastSu 2s 1 D
+R AQ 1972 o - F lastSu 2s 0 S
+R AQ 1989 1991 - O lastSu 2s 1 D
+R AQ 1990 1992 - Mar Su>=1 2s 0 S
+R Ho 1992 1993 - O lastSu 2s 1 D
+R Ho 1993 1994 - Mar Su>=1 2s 0 S
+Z Australia/Brisbane 10:12:8 - LMT 1895
+10 AU AE%sT 1971
+10 AQ AE%sT
+Z Australia/Lindeman 9:55:56 - LMT 1895
+10 AU AE%sT 1971
+10 AQ AE%sT 1992 Jul
+10 Ho AE%sT
+R AS 1971 1985 - O lastSu 2s 1 D
+R AS 1986 o - O 19 2s 1 D
+R AS 1987 2007 - O lastSu 2s 1 D
+R AS 1972 o - F 27 2s 0 S
+R AS 1973 1985 - Mar Su>=1 2s 0 S
+R AS 1986 1990 - Mar Su>=15 2s 0 S
+R AS 1991 o - Mar 3 2s 0 S
+R AS 1992 o - Mar 22 2s 0 S
+R AS 1993 o - Mar 7 2s 0 S
+R AS 1994 o - Mar 20 2s 0 S
+R AS 1995 2005 - Mar lastSu 2s 0 S
+R AS 2006 o - Ap 2 2s 0 S
+R AS 2007 o - Mar lastSu 2s 0 S
+R AS 2008 ma - Ap Su>=1 2s 0 S
+R AS 2008 ma - O Su>=1 2s 1 D
+Z Australia/Adelaide 9:14:20 - LMT 1895 F
+9 - ACST 1899 May
+9:30 AU AC%sT 1971
+9:30 AS AC%sT
+R AT 1916 o - O Su>=1 2s 1 D
+R AT 1917 o - Mar lastSu 2s 0 S
+R AT 1917 1918 - O Su>=22 2s 1 D
+R AT 1918 1919 - Mar Su>=1 2s 0 S
+R AT 1967 o - O Su>=1 2s 1 D
+R AT 1968 o - Mar Su>=29 2s 0 S
+R AT 1968 1985 - O lastSu 2s 1 D
+R AT 1969 1971 - Mar Su>=8 2s 0 S
+R AT 1972 o - F lastSu 2s 0 S
+R AT 1973 1981 - Mar Su>=1 2s 0 S
+R AT 1982 1983 - Mar lastSu 2s 0 S
+R AT 1984 1986 - Mar Su>=1 2s 0 S
+R AT 1986 o - O Su>=15 2s 1 D
+R AT 1987 1990 - Mar Su>=15 2s 0 S
+R AT 1987 o - O Su>=22 2s 1 D
+R AT 1988 1990 - O lastSu 2s 1 D
+R AT 1991 1999 - O Su>=1 2s 1 D
+R AT 1991 2005 - Mar lastSu 2s 0 S
+R AT 2000 o - Au lastSu 2s 1 D
+R AT 2001 ma - O Su>=1 2s 1 D
+R AT 2006 o - Ap Su>=1 2s 0 S
+R AT 2007 o - Mar lastSu 2s 0 S
+R AT 2008 ma - Ap Su>=1 2s 0 S
+Z Australia/Hobart 9:49:16 - LMT 1895 S
+10 AT AE%sT 1919 O 24
+10 AU AE%sT 1967
+10 AT AE%sT
+R AV 1971 1985 - O lastSu 2s 1 D
+R AV 1972 o - F lastSu 2s 0 S
+R AV 1973 1985 - Mar Su>=1 2s 0 S
+R AV 1986 1990 - Mar Su>=15 2s 0 S
+R AV 1986 1987 - O Su>=15 2s 1 D
+R AV 1988 1999 - O lastSu 2s 1 D
+R AV 1991 1994 - Mar Su>=1 2s 0 S
+R AV 1995 2005 - Mar lastSu 2s 0 S
+R AV 2000 o - Au lastSu 2s 1 D
+R AV 2001 2007 - O lastSu 2s 1 D
+R AV 2006 o - Ap Su>=1 2s 0 S
+R AV 2007 o - Mar lastSu 2s 0 S
+R AV 2008 ma - Ap Su>=1 2s 0 S
+R AV 2008 ma - O Su>=1 2s 1 D
+Z Australia/Melbourne 9:39:52 - LMT 1895 F
+10 AU AE%sT 1971
+10 AV AE%sT
+R AN 1971 1985 - O lastSu 2s 1 D
+R AN 1972 o - F 27 2s 0 S
+R AN 1973 1981 - Mar Su>=1 2s 0 S
+R AN 1982 o - Ap Su>=1 2s 0 S
+R AN 1983 1985 - Mar Su>=1 2s 0 S
+R AN 1986 1989 - Mar Su>=15 2s 0 S
+R AN 1986 o - O 19 2s 1 D
+R AN 1987 1999 - O lastSu 2s 1 D
+R AN 1990 1995 - Mar Su>=1 2s 0 S
+R AN 1996 2005 - Mar lastSu 2s 0 S
+R AN 2000 o - Au lastSu 2s 1 D
+R AN 2001 2007 - O lastSu 2s 1 D
+R AN 2006 o - Ap Su>=1 2s 0 S
+R AN 2007 o - Mar lastSu 2s 0 S
+R AN 2008 ma - Ap Su>=1 2s 0 S
+R AN 2008 ma - O Su>=1 2s 1 D
+Z Australia/Sydney 10:4:52 - LMT 1895 F
+10 AU AE%sT 1971
+10 AN AE%sT
+Z Australia/Broken_Hill 9:25:48 - LMT 1895 F
+10 - AEST 1896 Au 23
+9 - ACST 1899 May
+9:30 AU AC%sT 1971
+9:30 AN AC%sT 2000
+9:30 AS AC%sT
+R LH 1981 1984 - O lastSu 2 1 -
+R LH 1982 1985 - Mar Su>=1 2 0 -
+R LH 1985 o - O lastSu 2 0:30 -
+R LH 1986 1989 - Mar Su>=15 2 0 -
+R LH 1986 o - O 19 2 0:30 -
+R LH 1987 1999 - O lastSu 2 0:30 -
+R LH 1990 1995 - Mar Su>=1 2 0 -
+R LH 1996 2005 - Mar lastSu 2 0 -
+R LH 2000 o - Au lastSu 2 0:30 -
+R LH 2001 2007 - O lastSu 2 0:30 -
+R LH 2006 o - Ap Su>=1 2 0 -
+R LH 2007 o - Mar lastSu 2 0 -
+R LH 2008 ma - Ap Su>=1 2 0 -
+R LH 2008 ma - O Su>=1 2 0:30 -
+Z Australia/Lord_Howe 10:36:20 - LMT 1895 F
+10 - AEST 1981 Mar
+10:30 LH +1030/+1130 1985 Jul
+10:30 LH +1030/+11
+Z Antarctica/Macquarie 0 - -00 1899 N
+10 - AEST 1916 O 1 2
+10 1 AEDT 1917 F
+10 AU AE%sT 1919 Ap 1 0s
+0 - -00 1948 Mar 25
+10 AU AE%sT 1967
+10 AT AE%sT 2010
+10 1 AEDT 2011
+10 AT AE%sT
+Z Indian/Christmas 7:2:52 - LMT 1895 F
+7 - +07
+Z Indian/Cocos 6:27:40 - LMT 1900
+6:30 - +0630
+R FJ 1998 1999 - N Su>=1 2 1 -
+R FJ 1999 2000 - F lastSu 3 0 -
+R FJ 2009 o - N 29 2 1 -
+R FJ 2010 o - Mar lastSu 3 0 -
+R FJ 2010 2013 - O Su>=21 2 1 -
+R FJ 2011 o - Mar Su>=1 3 0 -
+R FJ 2012 2013 - Ja Su>=18 3 0 -
+R FJ 2014 o - Ja Su>=18 2 0 -
+R FJ 2014 2018 - N Su>=1 2 1 -
+R FJ 2015 2021 - Ja Su>=12 3 0 -
+R FJ 2019 o - N Su>=8 2 1 -
+R FJ 2020 o - D 20 2 1 -
+R FJ 2022 ma - N Su>=8 2 1 -
+R FJ 2023 ma - Ja Su>=12 3 0 -
+Z Pacific/Fiji 11:55:44 - LMT 1915 O 26
+12 FJ +12/+13
+Z Pacific/Gambier -8:59:48 - LMT 1912 O
+-9 - -09
+Z Pacific/Marquesas -9:18 - LMT 1912 O
+-9:30 - -0930
+Z Pacific/Tahiti -9:58:16 - LMT 1912 O
+-10 - -10
+R Gu 1959 o - Jun 27 2 1 D
+R Gu 1961 o - Ja 29 2 0 S
+R Gu 1967 o - S 1 2 1 D
+R Gu 1969 o - Ja 26 0:1 0 S
+R Gu 1969 o - Jun 22 2 1 D
+R Gu 1969 o - Au 31 2 0 S
+R Gu 1970 1971 - Ap lastSu 2 1 D
+R Gu 1970 1971 - S Su>=1 2 0 S
+R Gu 1973 o - D 16 2 1 D
+R Gu 1974 o - F 24 2 0 S
+R Gu 1976 o - May 26 2 1 D
+R Gu 1976 o - Au 22 2:1 0 S
+R Gu 1977 o - Ap 24 2 1 D
+R Gu 1977 o - Au 28 2 0 S
+Z Pacific/Guam -14:21 - LMT 1844 D 31
+9:39 - LMT 1901
+10 - GST 1941 D 10
+9 - +09 1944 Jul 31
+10 Gu G%sT 2000 D 23
+10 - ChST
+L Pacific/Guam Pacific/Saipan
+Z Pacific/Tarawa 11:32:4 - LMT 1901
+12 - +12
+Z Pacific/Kanton 0 - -00 1937 Au 31
+-12 - -12 1979 O
+-11 - -11 1994 D 31
+13 - +13
+Z Pacific/Kiritimati -10:29:20 - LMT 1901
+-10:40 - -1040 1979 O
+-10 - -10 1994 D 31
+14 - +14
+Z Pacific/Majuro 11:24:48 - LMT 1901
+11 - +11 1914 O
+9 - +09 1919 F
+11 - +11 1937
+10 - +10 1941 Ap
+9 - +09 1944 Ja 30
+11 - +11 1969 O
+12 - +12
+Z Pacific/Kwajalein 11:9:20 - LMT 1901
+11 - +11 1937
+10 - +10 1941 Ap
+9 - +09 1944 F 6
+11 - +11 1969 O
+-12 - -12 1993 Au 20 24
+12 - +12
+Z Pacific/Chuuk -13:52:52 - LMT 1844 D 31
+10:7:8 - LMT 1901
+10 - +10 1914 O
+9 - +09 1919 F
+10 - +10 1941 Ap
+9 - +09 1945 Au
+10 - +10
+Z Pacific/Pohnpei -13:27:8 - LMT 1844 D 31
+10:32:52 - LMT 1901
+11 - +11 1914 O
+9 - +09 1919 F
+11 - +11 1937
+10 - +10 1941 Ap
+9 - +09 1945 Au
+11 - +11
+Z Pacific/Kosrae -13:8:4 - LMT 1844 D 31
+10:51:56 - LMT 1901
+11 - +11 1914 O
+9 - +09 1919 F
+11 - +11 1937
+10 - +10 1941 Ap
+9 - +09 1945 Au
+11 - +11 1969 O
+12 - +12 1999
+11 - +11
+Z Pacific/Nauru 11:7:40 - LMT 1921 Ja 15
+11:30 - +1130 1942 Au 29
+9 - +09 1945 S 8
+11:30 - +1130 1979 F 10 2
+12 - +12
+R NC 1977 1978 - D Su>=1 0 1 -
+R NC 1978 1979 - F 27 0 0 -
+R NC 1996 o - D 1 2s 1 -
+R NC 1997 o - Mar 2 2s 0 -
+Z Pacific/Noumea 11:5:48 - LMT 1912 Ja 13
+11 NC +11/+12
+R NZ 1927 o - N 6 2 1 S
+R NZ 1928 o - Mar 4 2 0 M
+R NZ 1928 1933 - O Su>=8 2 0:30 S
+R NZ 1929 1933 - Mar Su>=15 2 0 M
+R NZ 1934 1940 - Ap lastSu 2 0 M
+R NZ 1934 1940 - S lastSu 2 0:30 S
+R NZ 1946 o - Ja 1 0 0 S
+R NZ 1974 o - N Su>=1 2s 1 D
+R k 1974 o - N Su>=1 2:45s 1 -
+R NZ 1975 o - F lastSu 2s 0 S
+R k 1975 o - F lastSu 2:45s 0 -
+R NZ 1975 1988 - O lastSu 2s 1 D
+R k 1975 1988 - O lastSu 2:45s 1 -
+R NZ 1976 1989 - Mar Su>=1 2s 0 S
+R k 1976 1989 - Mar Su>=1 2:45s 0 -
+R NZ 1989 o - O Su>=8 2s 1 D
+R k 1989 o - O Su>=8 2:45s 1 -
+R NZ 1990 2006 - O Su>=1 2s 1 D
+R k 1990 2006 - O Su>=1 2:45s 1 -
+R NZ 1990 2007 - Mar Su>=15 2s 0 S
+R k 1990 2007 - Mar Su>=15 2:45s 0 -
+R NZ 2007 ma - S lastSu 2s 1 D
+R k 2007 ma - S lastSu 2:45s 1 -
+R NZ 2008 ma - Ap Su>=1 2s 0 S
+R k 2008 ma - Ap Su>=1 2:45s 0 -
+Z Pacific/Auckland 11:39:4 - LMT 1868 N 2
+11:30 NZ NZ%sT 1946
+12 NZ NZ%sT
+Z Pacific/Chatham 12:13:48 - LMT 1868 N 2
+12:15 - +1215 1946
+12:45 k +1245/+1345
+L Pacific/Auckland Antarctica/McMurdo
+R CK 1978 o - N 12 0 0:30 -
+R CK 1979 1991 - Mar Su>=1 0 0 -
+R CK 1979 1990 - O lastSu 0 0:30 -
+Z Pacific/Rarotonga 13:20:56 - LMT 1899 D 26
+-10:39:4 - LMT 1952 O 16
+-10:30 - -1030 1978 N 12
+-10 CK -10/-0930
+Z Pacific/Niue -11:19:40 - LMT 1952 O 16
+-11:20 - -1120 1964 Jul
+-11 - -11
+Z Pacific/Norfolk 11:11:52 - LMT 1901
+11:12 - +1112 1951
+11:30 - +1130 1974 O 27 2s
+11:30 1 +1230 1975 Mar 2 2s
+11:30 - +1130 2015 O 4 2s
+11 - +11 2019 Jul
+11 AN +11/+12
+Z Pacific/Palau -15:2:4 - LMT 1844 D 31
+8:57:56 - LMT 1901
+9 - +09
+Z Pacific/Port_Moresby 9:48:40 - LMT 1880
+9:48:32 - PMMT 1895
+10 - +10
+L Pacific/Port_Moresby Antarctica/DumontDUrville
+Z Pacific/Bougainville 10:22:16 - LMT 1880
+9:48:32 - PMMT 1895
+10 - +10 1942 Jul
+9 - +09 1945 Au 21
+10 - +10 2014 D 28 2
+11 - +11
+Z Pacific/Pitcairn -8:40:20 - LMT 1901
+-8:30 - -0830 1998 Ap 27
+-8 - -08
+Z Pacific/Pago_Pago 12:37:12 - LMT 1892 Jul 5
+-11:22:48 - LMT 1911
+-11 - SST
+L Pacific/Pago_Pago Pacific/Midway
+R WS 2010 o - S lastSu 0 1 -
+R WS 2011 o - Ap Sa>=1 4 0 -
+R WS 2011 o - S lastSa 3 1 -
+R WS 2012 2021 - Ap Su>=1 4 0 -
+R WS 2012 2020 - S lastSu 3 1 -
+Z Pacific/Apia 12:33:4 - LMT 1892 Jul 5
+-11:26:56 - LMT 1911
+-11:30 - -1130 1950
+-11 WS -11/-10 2011 D 29 24
+13 WS +13/+14
+Z Pacific/Guadalcanal 10:39:48 - LMT 1912 O
+11 - +11
+Z Pacific/Fakaofo -11:24:56 - LMT 1901
+-11 - -11 2011 D 30
+13 - +13
+R TO 1999 o - O 7 2s 1 -
+R TO 2000 o - Mar 19 2s 0 -
+R TO 2000 2001 - N Su>=1 2 1 -
+R TO 2001 2002 - Ja lastSu 2 0 -
+R TO 2016 o - N Su>=1 2 1 -
+R TO 2017 o - Ja Su>=15 3 0 -
+Z Pacific/Tongatapu 12:19:12 - LMT 1945 S 10
+12:20 - +1220 1961
+13 - +13 1999
+13 TO +13/+14
+Z Pacific/Funafuti 11:56:52 - LMT 1901
+12 - +12
+Z Pacific/Wake 11:6:28 - LMT 1901
+12 - +12
+R VU 1973 o - D 22 12u 1 -
+R VU 1974 o - Mar 30 12u 0 -
+R VU 1983 1991 - S Sa>=22 24 1 -
+R VU 1984 1991 - Mar Sa>=22 24 0 -
+R VU 1992 1993 - Ja Sa>=22 24 0 -
+R VU 1992 o - O Sa>=22 24 1 -
+Z Pacific/Efate 11:13:16 - LMT 1912 Ja 13
+11 VU +11/+12
+Z Pacific/Wallis 12:15:20 - LMT 1901
+12 - +12
+R G 1916 o - May 21 2s 1 BST
+R G 1916 o - O 1 2s 0 GMT
+R G 1917 o - Ap 8 2s 1 BST
+R G 1917 o - S 17 2s 0 GMT
+R G 1918 o - Mar 24 2s 1 BST
+R G 1918 o - S 30 2s 0 GMT
+R G 1919 o - Mar 30 2s 1 BST
+R G 1919 o - S 29 2s 0 GMT
+R G 1920 o - Mar 28 2s 1 BST
+R G 1920 o - O 25 2s 0 GMT
+R G 1921 o - Ap 3 2s 1 BST
+R G 1921 o - O 3 2s 0 GMT
+R G 1922 o - Mar 26 2s 1 BST
+R G 1922 o - O 8 2s 0 GMT
+R G 1923 o - Ap Su>=16 2s 1 BST
+R G 1923 1924 - S Su>=16 2s 0 GMT
+R G 1924 o - Ap Su>=9 2s 1 BST
+R G 1925 1926 - Ap Su>=16 2s 1 BST
+R G 1925 1938 - O Su>=2 2s 0 GMT
+R G 1927 o - Ap Su>=9 2s 1 BST
+R G 1928 1929 - Ap Su>=16 2s 1 BST
+R G 1930 o - Ap Su>=9 2s 1 BST
+R G 1931 1932 - Ap Su>=16 2s 1 BST
+R G 1933 o - Ap Su>=9 2s 1 BST
+R G 1934 o - Ap Su>=16 2s 1 BST
+R G 1935 o - Ap Su>=9 2s 1 BST
+R G 1936 1937 - Ap Su>=16 2s 1 BST
+R G 1938 o - Ap Su>=9 2s 1 BST
+R G 1939 o - Ap Su>=16 2s 1 BST
+R G 1939 o - N Su>=16 2s 0 GMT
+R G 1940 o - F Su>=23 2s 1 BST
+R G 1941 o - May Su>=2 1s 2 BDST
+R G 1941 1943 - Au Su>=9 1s 1 BST
+R G 1942 1944 - Ap Su>=2 1s 2 BDST
+R G 1944 o - S Su>=16 1s 1 BST
+R G 1945 o - Ap M>=2 1s 2 BDST
+R G 1945 o - Jul Su>=9 1s 1 BST
+R G 1945 1946 - O Su>=2 2s 0 GMT
+R G 1946 o - Ap Su>=9 2s 1 BST
+R G 1947 o - Mar 16 2s 1 BST
+R G 1947 o - Ap 13 1s 2 BDST
+R G 1947 o - Au 10 1s 1 BST
+R G 1947 o - N 2 2s 0 GMT
+R G 1948 o - Mar 14 2s 1 BST
+R G 1948 o - O 31 2s 0 GMT
+R G 1949 o - Ap 3 2s 1 BST
+R G 1949 o - O 30 2s 0 GMT
+R G 1950 1952 - Ap Su>=14 2s 1 BST
+R G 1950 1952 - O Su>=21 2s 0 GMT
+R G 1953 o - Ap Su>=16 2s 1 BST
+R G 1953 1960 - O Su>=2 2s 0 GMT
+R G 1954 o - Ap Su>=9 2s 1 BST
+R G 1955 1956 - Ap Su>=16 2s 1 BST
+R G 1957 o - Ap Su>=9 2s 1 BST
+R G 1958 1959 - Ap Su>=16 2s 1 BST
+R G 1960 o - Ap Su>=9 2s 1 BST
+R G 1961 1963 - Mar lastSu 2s 1 BST
+R G 1961 1968 - O Su>=23 2s 0 GMT
+R G 1964 1967 - Mar Su>=19 2s 1 BST
+R G 1968 o - F 18 2s 1 BST
+R G 1972 1980 - Mar Su>=16 2s 1 BST
+R G 1972 1980 - O Su>=23 2s 0 GMT
+R G 1981 1995 - Mar lastSu 1u 1 BST
+R G 1981 1989 - O Su>=23 1u 0 GMT
+R G 1990 1995 - O Su>=22 1u 0 GMT
+Z Europe/London -0:1:15 - LMT 1847 D 1 0s
+0 G %s 1968 O 27
+1 - BST 1971 O 31 2u
+0 G %s 1996
+0 E GMT/BST
+L Europe/London Europe/Jersey
+L Europe/London Europe/Guernsey
+L Europe/London Europe/Isle_of_Man
+R IE 1971 o - O 31 2u -1 -
+R IE 1972 1980 - Mar Su>=16 2u 0 -
+R IE 1972 1980 - O Su>=23 2u -1 -
+R IE 1981 ma - Mar lastSu 1u 0 -
+R IE 1981 1989 - O Su>=23 1u -1 -
+R IE 1990 1995 - O Su>=22 1u -1 -
+R IE 1996 ma - O lastSu 1u -1 -
+Z Europe/Dublin -0:25 - LMT 1880 Au 2
+-0:25:21 - DMT 1916 May 21 2s
+-0:25:21 1 IST 1916 O 1 2s
+0 G %s 1921 D 6
+0 G GMT/IST 1940 F 25 2s
+0 1 IST 1946 O 6 2s
+0 - GMT 1947 Mar 16 2s
+0 1 IST 1947 N 2 2s
+0 - GMT 1948 Ap 18 2s
+0 G GMT/IST 1968 O 27
+1 IE IST/GMT
+R E 1977 1980 - Ap Su>=1 1u 1 S
+R E 1977 o - S lastSu 1u 0 -
+R E 1978 o - O 1 1u 0 -
+R E 1979 1995 - S lastSu 1u 0 -
+R E 1981 ma - Mar lastSu 1u 1 S
+R E 1996 ma - O lastSu 1u 0 -
+R W- 1977 1980 - Ap Su>=1 1s 1 S
+R W- 1977 o - S lastSu 1s 0 -
+R W- 1978 o - O 1 1s 0 -
+R W- 1979 1995 - S lastSu 1s 0 -
+R W- 1981 ma - Mar lastSu 1s 1 S
+R W- 1996 ma - O lastSu 1s 0 -
+R c 1916 o - Ap 30 23 1 S
+R c 1916 o - O 1 1 0 -
+R c 1917 1918 - Ap M>=15 2s 1 S
+R c 1917 1918 - S M>=15 2s 0 -
+R c 1940 o - Ap 1 2s 1 S
+R c 1942 o - N 2 2s 0 -
+R c 1943 o - Mar 29 2s 1 S
+R c 1943 o - O 4 2s 0 -
+R c 1944 1945 - Ap M>=1 2s 1 S
+R c 1944 o - O 2 2s 0 -
+R c 1945 o - S 16 2s 0 -
+R c 1977 1980 - Ap Su>=1 2s 1 S
+R c 1977 o - S lastSu 2s 0 -
+R c 1978 o - O 1 2s 0 -
+R c 1979 1995 - S lastSu 2s 0 -
+R c 1981 ma - Mar lastSu 2s 1 S
+R c 1996 ma - O lastSu 2s 0 -
+R e 1977 1980 - Ap Su>=1 0 1 S
+R e 1977 o - S lastSu 0 0 -
+R e 1978 o - O 1 0 0 -
+R e 1979 1995 - S lastSu 0 0 -
+R e 1981 ma - Mar lastSu 0 1 S
+R e 1996 ma - O lastSu 0 0 -
+R R 1917 o - Jul 1 23 1 MST
+R R 1917 o - D 28 0 0 MMT
+R R 1918 o - May 31 22 2 MDST
+R R 1918 o - S 16 1 1 MST
+R R 1919 o - May 31 23 2 MDST
+R R 1919 o - Jul 1 0u 1 MSD
+R R 1919 o - Au 16 0 0 MSK
+R R 1921 o - F 14 23 1 MSD
+R R 1921 o - Mar 20 23 2 +05
+R R 1921 o - S 1 0 1 MSD
+R R 1921 o - O 1 0 0 -
+R R 1981 1984 - Ap 1 0 1 S
+R R 1981 1983 - O 1 0 0 -
+R R 1984 1995 - S lastSu 2s 0 -
+R R 1985 2010 - Mar lastSu 2s 1 S
+R R 1996 2010 - O lastSu 2s 0 -
+Z WET 0 E WE%sT
+Z CET 1 c CE%sT
+Z MET 1 c ME%sT
+Z EET 2 E EE%sT
+R q 1940 o - Jun 16 0 1 S
+R q 1942 o - N 2 3 0 -
+R q 1943 o - Mar 29 2 1 S
+R q 1943 o - Ap 10 3 0 -
+R q 1974 o - May 4 0 1 S
+R q 1974 o - O 2 0 0 -
+R q 1975 o - May 1 0 1 S
+R q 1975 o - O 2 0 0 -
+R q 1976 o - May 2 0 1 S
+R q 1976 o - O 3 0 0 -
+R q 1977 o - May 8 0 1 S
+R q 1977 o - O 2 0 0 -
+R q 1978 o - May 6 0 1 S
+R q 1978 o - O 1 0 0 -
+R q 1979 o - May 5 0 1 S
+R q 1979 o - S 30 0 0 -
+R q 1980 o - May 3 0 1 S
+R q 1980 o - O 4 0 0 -
+R q 1981 o - Ap 26 0 1 S
+R q 1981 o - S 27 0 0 -
+R q 1982 o - May 2 0 1 S
+R q 1982 o - O 3 0 0 -
+R q 1983 o - Ap 18 0 1 S
+R q 1983 o - O 1 0 0 -
+R q 1984 o - Ap 1 0 1 S
+Z Europe/Tirane 1:19:20 - LMT 1914
+1 - CET 1940 Jun 16
+1 q CE%sT 1984 Jul
+1 E CE%sT
+Z Europe/Andorra 0:6:4 - LMT 1901
+0 - WET 1946 S 30
+1 - CET 1985 Mar 31 2
+1 E CE%sT
+R a 1920 o - Ap 5 2s 1 S
+R a 1920 o - S 13 2s 0 -
+R a 1946 o - Ap 14 2s 1 S
+R a 1946 o - O 7 2s 0 -
+R a 1947 1948 - O Su>=1 2s 0 -
+R a 1947 o - Ap 6 2s 1 S
+R a 1948 o - Ap 18 2s 1 S
+R a 1980 o - Ap 6 0 1 S
+R a 1980 o - S 28 0 0 -
+Z Europe/Vienna 1:5:21 - LMT 1893 Ap
+1 c CE%sT 1920
+1 a CE%sT 1940 Ap 1 2s
+1 c CE%sT 1945 Ap 2 2s
+1 1 CEST 1945 Ap 12 2s
+1 - CET 1946
+1 a CE%sT 1981
+1 E CE%sT
+Z Europe/Minsk 1:50:16 - LMT 1880
+1:50 - MMT 1924 May 2
+2 - EET 1930 Jun 21
+3 - MSK 1941 Jun 28
+1 c CE%sT 1944 Jul 3
+3 R MSK/MSD 1990
+3 - MSK 1991 Mar 31 2s
+2 R EE%sT 2011 Mar 27 2s
+3 - +03
+R b 1918 o - Mar 9 0s 1 S
+R b 1918 1919 - O Sa>=1 23s 0 -
+R b 1919 o - Mar 1 23s 1 S
+R b 1920 o - F 14 23s 1 S
+R b 1920 o - O 23 23s 0 -
+R b 1921 o - Mar 14 23s 1 S
+R b 1921 o - O 25 23s 0 -
+R b 1922 o - Mar 25 23s 1 S
+R b 1922 1927 - O Sa>=1 23s 0 -
+R b 1923 o - Ap 21 23s 1 S
+R b 1924 o - Mar 29 23s 1 S
+R b 1925 o - Ap 4 23s 1 S
+R b 1926 o - Ap 17 23s 1 S
+R b 1927 o - Ap 9 23s 1 S
+R b 1928 o - Ap 14 23s 1 S
+R b 1928 1938 - O Su>=2 2s 0 -
+R b 1929 o - Ap 21 2s 1 S
+R b 1930 o - Ap 13 2s 1 S
+R b 1931 o - Ap 19 2s 1 S
+R b 1932 o - Ap 3 2s 1 S
+R b 1933 o - Mar 26 2s 1 S
+R b 1934 o - Ap 8 2s 1 S
+R b 1935 o - Mar 31 2s 1 S
+R b 1936 o - Ap 19 2s 1 S
+R b 1937 o - Ap 4 2s 1 S
+R b 1938 o - Mar 27 2s 1 S
+R b 1939 o - Ap 16 2s 1 S
+R b 1939 o - N 19 2s 0 -
+R b 1940 o - F 25 2s 1 S
+R b 1944 o - S 17 2s 0 -
+R b 1945 o - Ap 2 2s 1 S
+R b 1945 o - S 16 2s 0 -
+R b 1946 o - May 19 2s 1 S
+R b 1946 o - O 7 2s 0 -
+Z Europe/Brussels 0:17:30 - LMT 1880
+0:17:30 - BMT 1892 May 1 0:17:30
+0 - WET 1914 N 8
+1 - CET 1916 May
+1 c CE%sT 1918 N 11 11u
+0 b WE%sT 1940 May 20 2s
+1 c CE%sT 1944 S 3
+1 b CE%sT 1977
+1 E CE%sT
+R BG 1979 o - Mar 31 23 1 S
+R BG 1979 o - O 1 1 0 -
+R BG 1980 1982 - Ap Sa>=1 23 1 S
+R BG 1980 o - S 29 1 0 -
+R BG 1981 o - S 27 2 0 -
+Z Europe/Sofia 1:33:16 - LMT 1880
+1:56:56 - IMT 1894 N 30
+2 - EET 1942 N 2 3
+1 c CE%sT 1945
+1 - CET 1945 Ap 2 3
+2 - EET 1979 Mar 31 23
+2 BG EE%sT 1982 S 26 3
+2 c EE%sT 1991
+2 e EE%sT 1997
+2 E EE%sT
+R CZ 1945 o - Ap M>=1 2s 1 S
+R CZ 1945 o - O 1 2s 0 -
+R CZ 1946 o - May 6 2s 1 S
+R CZ 1946 1949 - O Su>=1 2s 0 -
+R CZ 1947 1948 - Ap Su>=15 2s 1 S
+R CZ 1949 o - Ap 9 2s 1 S
+Z Europe/Prague 0:57:44 - LMT 1850
+0:57:44 - PMT 1891 O
+1 c CE%sT 1945 May 9
+1 CZ CE%sT 1946 D 1 3
+1 -1 GMT 1947 F 23 2
+1 CZ CE%sT 1979
+1 E CE%sT
+R D 1916 o - May 14 23 1 S
+R D 1916 o - S 30 23 0 -
+R D 1940 o - May 15 0 1 S
+R D 1945 o - Ap 2 2s 1 S
+R D 1945 o - Au 15 2s 0 -
+R D 1946 o - May 1 2s 1 S
+R D 1946 o - S 1 2s 0 -
+R D 1947 o - May 4 2s 1 S
+R D 1947 o - Au 10 2s 0 -
+R D 1948 o - May 9 2s 1 S
+R D 1948 o - Au 8 2s 0 -
+Z Europe/Copenhagen 0:50:20 - LMT 1890
+0:50:20 - CMT 1894
+1 D CE%sT 1942 N 2 2s
+1 c CE%sT 1945 Ap 2 2
+1 D CE%sT 1980
+1 E CE%sT
+Z Atlantic/Faroe -0:27:4 - LMT 1908 Ja 11
+0 - WET 1981
+0 E WE%sT
+R Th 1991 1992 - Mar lastSu 2 1 D
+R Th 1991 1992 - S lastSu 2 0 S
+R Th 1993 2006 - Ap Su>=1 2 1 D
+R Th 1993 2006 - O lastSu 2 0 S
+R Th 2007 ma - Mar Su>=8 2 1 D
+R Th 2007 ma - N Su>=1 2 0 S
+Z America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28
+-3 - -03 1980 Ap 6 2
+-3 E -03/-02 1996
+0 - GMT
+Z America/Scoresbysund -1:27:52 - LMT 1916 Jul 28
+-2 - -02 1980 Ap 6 2
+-2 c -02/-01 1981 Mar 29
+-1 E -01/+00
+Z America/Nuuk -3:26:56 - LMT 1916 Jul 28
+-3 - -03 1980 Ap 6 2
+-3 E -03/-02
+Z America/Thule -4:35:8 - LMT 1916 Jul 28
+-4 Th A%sT
+Z Europe/Tallinn 1:39 - LMT 1880
+1:39 - TMT 1918 F
+1 c CE%sT 1919 Jul
+1:39 - TMT 1921 May
+2 - EET 1940 Au 6
+3 - MSK 1941 S 15
+1 c CE%sT 1944 S 22
+3 R MSK/MSD 1989 Mar 26 2s
+2 1 EEST 1989 S 24 2s
+2 c EE%sT 1998 S 22
+2 E EE%sT 1999 O 31 4
+2 - EET 2002 F 21
+2 E EE%sT
+R FI 1942 o - Ap 2 24 1 S
+R FI 1942 o - O 4 1 0 -
+R FI 1981 1982 - Mar lastSu 2 1 S
+R FI 1981 1982 - S lastSu 3 0 -
+Z Europe/Helsinki 1:39:49 - LMT 1878 May 31
+1:39:49 - HMT 1921 May
+2 FI EE%sT 1983
+2 E EE%sT
+L Europe/Helsinki Europe/Mariehamn
+R F 1916 o - Jun 14 23s 1 S
+R F 1916 1919 - O Su>=1 23s 0 -
+R F 1917 o - Mar 24 23s 1 S
+R F 1918 o - Mar 9 23s 1 S
+R F 1919 o - Mar 1 23s 1 S
+R F 1920 o - F 14 23s 1 S
+R F 1920 o - O 23 23s 0 -
+R F 1921 o - Mar 14 23s 1 S
+R F 1921 o - O 25 23s 0 -
+R F 1922 o - Mar 25 23s 1 S
+R F 1922 1938 - O Sa>=1 23s 0 -
+R F 1923 o - May 26 23s 1 S
+R F 1924 o - Mar 29 23s 1 S
+R F 1925 o - Ap 4 23s 1 S
+R F 1926 o - Ap 17 23s 1 S
+R F 1927 o - Ap 9 23s 1 S
+R F 1928 o - Ap 14 23s 1 S
+R F 1929 o - Ap 20 23s 1 S
+R F 1930 o - Ap 12 23s 1 S
+R F 1931 o - Ap 18 23s 1 S
+R F 1932 o - Ap 2 23s 1 S
+R F 1933 o - Mar 25 23s 1 S
+R F 1934 o - Ap 7 23s 1 S
+R F 1935 o - Mar 30 23s 1 S
+R F 1936 o - Ap 18 23s 1 S
+R F 1937 o - Ap 3 23s 1 S
+R F 1938 o - Mar 26 23s 1 S
+R F 1939 o - Ap 15 23s 1 S
+R F 1939 o - N 18 23s 0 -
+R F 1940 o - F 25 2 1 S
+R F 1941 o - May 5 0 2 M
+R F 1941 o - O 6 0 1 S
+R F 1942 o - Mar 9 0 2 M
+R F 1942 o - N 2 3 1 S
+R F 1943 o - Mar 29 2 2 M
+R F 1943 o - O 4 3 1 S
+R F 1944 o - Ap 3 2 2 M
+R F 1944 o - O 8 1 1 S
+R F 1945 o - Ap 2 2 2 M
+R F 1945 o - S 16 3 0 -
+R F 1976 o - Mar 28 1 1 S
+R F 1976 o - S 26 1 0 -
+Z Europe/Paris 0:9:21 - LMT 1891 Mar 16
+0:9:21 - PMT 1911 Mar 11
+0 F WE%sT 1940 Jun 14 23
+1 c CE%sT 1944 Au 25
+0 F WE%sT 1945 S 16 3
+1 F CE%sT 1977
+1 E CE%sT
+R DE 1946 o - Ap 14 2s 1 S
+R DE 1946 o - O 7 2s 0 -
+R DE 1947 1949 - O Su>=1 2s 0 -
+R DE 1947 o - Ap 6 3s 1 S
+R DE 1947 o - May 11 2s 2 M
+R DE 1947 o - Jun 29 3 1 S
+R DE 1948 o - Ap 18 2s 1 S
+R DE 1949 o - Ap 10 2s 1 S
+R So 1945 o - May 24 2 2 M
+R So 1945 o - S 24 3 1 S
+R So 1945 o - N 18 2s 0 -
+Z Europe/Berlin 0:53:28 - LMT 1893 Ap
+1 c CE%sT 1945 May 24 2
+1 So CE%sT 1946
+1 DE CE%sT 1980
+1 E CE%sT
+L Europe/Zurich Europe/Busingen
+Z Europe/Gibraltar -0:21:24 - LMT 1880 Au 2 0s
+0 G %s 1957 Ap 14 2
+1 - CET 1982
+1 E CE%sT
+R g 1932 o - Jul 7 0 1 S
+R g 1932 o - S 1 0 0 -
+R g 1941 o - Ap 7 0 1 S
+R g 1942 o - N 2 3 0 -
+R g 1943 o - Mar 30 0 1 S
+R g 1943 o - O 4 0 0 -
+R g 1952 o - Jul 1 0 1 S
+R g 1952 o - N 2 0 0 -
+R g 1975 o - Ap 12 0s 1 S
+R g 1975 o - N 26 0s 0 -
+R g 1976 o - Ap 11 2s 1 S
+R g 1976 o - O 10 2s 0 -
+R g 1977 1978 - Ap Su>=1 2s 1 S
+R g 1977 o - S 26 2s 0 -
+R g 1978 o - S 24 4 0 -
+R g 1979 o - Ap 1 9 1 S
+R g 1979 o - S 29 2 0 -
+R g 1980 o - Ap 1 0 1 S
+R g 1980 o - S 28 0 0 -
+Z Europe/Athens 1:34:52 - LMT 1895 S 14
+1:34:52 - AMT 1916 Jul 28 0:1
+2 g EE%sT 1941 Ap 30
+1 g CE%sT 1944 Ap 4
+2 g EE%sT 1981
+2 E EE%sT
+R h 1918 1919 - Ap 15 2 1 S
+R h 1918 1920 - S M>=15 3 0 -
+R h 1920 o - Ap 5 2 1 S
+R h 1945 o - May 1 23 1 S
+R h 1945 o - N 1 1 0 -
+R h 1946 o - Mar 31 2s 1 S
+R h 1946 o - O 7 2 0 -
+R h 1947 1949 - Ap Su>=4 2s 1 S
+R h 1947 1949 - O Su>=1 2s 0 -
+R h 1954 o - May 23 0 1 S
+R h 1954 o - O 3 0 0 -
+R h 1955 o - May 22 2 1 S
+R h 1955 o - O 2 3 0 -
+R h 1956 1957 - Jun Su>=1 2 1 S
+R h 1956 1957 - S lastSu 3 0 -
+R h 1980 o - Ap 6 0 1 S
+R h 1980 o - S 28 1 0 -
+R h 1981 1983 - Mar lastSu 0 1 S
+R h 1981 1983 - S lastSu 1 0 -
+Z Europe/Budapest 1:16:20 - LMT 1890 N
+1 c CE%sT 1918
+1 h CE%sT 1941 Ap 7 23
+1 c CE%sT 1945
+1 h CE%sT 1984
+1 E CE%sT
+R w 1917 1919 - F 19 23 1 -
+R w 1917 o - O 21 1 0 -
+R w 1918 1919 - N 16 1 0 -
+R w 1921 o - Mar 19 23 1 -
+R w 1921 o - Jun 23 1 0 -
+R w 1939 o - Ap 29 23 1 -
+R w 1939 o - O 29 2 0 -
+R w 1940 o - F 25 2 1 -
+R w 1940 1941 - N Su>=2 1s 0 -
+R w 1941 1942 - Mar Su>=2 1s 1 -
+R w 1943 1946 - Mar Su>=1 1s 1 -
+R w 1942 1948 - O Su>=22 1s 0 -
+R w 1947 1967 - Ap Su>=1 1s 1 -
+R w 1949 o - O 30 1s 0 -
+R w 1950 1966 - O Su>=22 1s 0 -
+R w 1967 o - O 29 1s 0 -
+Z Atlantic/Reykjavik -1:28 - LMT 1908
+-1 w -01/+00 1968 Ap 7 1s
+0 - GMT
+R I 1916 o - Jun 3 24 1 S
+R I 1916 1917 - S 30 24 0 -
+R I 1917 o - Mar 31 24 1 S
+R I 1918 o - Mar 9 24 1 S
+R I 1918 o - O 6 24 0 -
+R I 1919 o - Mar 1 24 1 S
+R I 1919 o - O 4 24 0 -
+R I 1920 o - Mar 20 24 1 S
+R I 1920 o - S 18 24 0 -
+R I 1940 o - Jun 14 24 1 S
+R I 1942 o - N 2 2s 0 -
+R I 1943 o - Mar 29 2s 1 S
+R I 1943 o - O 4 2s 0 -
+R I 1944 o - Ap 2 2s 1 S
+R I 1944 o - S 17 2s 0 -
+R I 1945 o - Ap 2 2 1 S
+R I 1945 o - S 15 1 0 -
+R I 1946 o - Mar 17 2s 1 S
+R I 1946 o - O 6 2s 0 -
+R I 1947 o - Mar 16 0s 1 S
+R I 1947 o - O 5 0s 0 -
+R I 1948 o - F 29 2s 1 S
+R I 1948 o - O 3 2s 0 -
+R I 1966 1968 - May Su>=22 0s 1 S
+R I 1966 o - S 24 24 0 -
+R I 1967 1969 - S Su>=22 0s 0 -
+R I 1969 o - Jun 1 0s 1 S
+R I 1970 o - May 31 0s 1 S
+R I 1970 o - S lastSu 0s 0 -
+R I 1971 1972 - May Su>=22 0s 1 S
+R I 1971 o - S lastSu 0s 0 -
+R I 1972 o - O 1 0s 0 -
+R I 1973 o - Jun 3 0s 1 S
+R I 1973 1974 - S lastSu 0s 0 -
+R I 1974 o - May 26 0s 1 S
+R I 1975 o - Jun 1 0s 1 S
+R I 1975 1977 - S lastSu 0s 0 -
+R I 1976 o - May 30 0s 1 S
+R I 1977 1979 - May Su>=22 0s 1 S
+R I 1978 o - O 1 0s 0 -
+R I 1979 o - S 30 0s 0 -
+Z Europe/Rome 0:49:56 - LMT 1866 D 12
+0:49:56 - RMT 1893 O 31 23:49:56
+1 I CE%sT 1943 S 10
+1 c CE%sT 1944 Jun 4
+1 I CE%sT 1980
+1 E CE%sT
+L Europe/Rome Europe/Vatican
+L Europe/Rome Europe/San_Marino
+R LV 1989 1996 - Mar lastSu 2s 1 S
+R LV 1989 1996 - S lastSu 2s 0 -
+Z Europe/Riga 1:36:34 - LMT 1880
+1:36:34 - RMT 1918 Ap 15 2
+1:36:34 1 LST 1918 S 16 3
+1:36:34 - RMT 1919 Ap 1 2
+1:36:34 1 LST 1919 May 22 3
+1:36:34 - RMT 1926 May 11
+2 - EET 1940 Au 5
+3 - MSK 1941 Jul
+1 c CE%sT 1944 O 13
+3 R MSK/MSD 1989 Mar lastSu 2s
+2 1 EEST 1989 S lastSu 2s
+2 LV EE%sT 1997 Ja 21
+2 E EE%sT 2000 F 29
+2 - EET 2001 Ja 2
+2 E EE%sT
+L Europe/Zurich Europe/Vaduz
+Z Europe/Vilnius 1:41:16 - LMT 1880
+1:24 - WMT 1917
+1:35:36 - KMT 1919 O 10
+1 - CET 1920 Jul 12
+2 - EET 1920 O 9
+1 - CET 1940 Au 3
+3 - MSK 1941 Jun 24
+1 c CE%sT 1944 Au
+3 R MSK/MSD 1989 Mar 26 2s
+2 R EE%sT 1991 S 29 2s
+2 c EE%sT 1998
+2 - EET 1998 Mar 29 1u
+1 E CE%sT 1999 O 31 1u
+2 - EET 2003
+2 E EE%sT
+R LX 1916 o - May 14 23 1 S
+R LX 1916 o - O 1 1 0 -
+R LX 1917 o - Ap 28 23 1 S
+R LX 1917 o - S 17 1 0 -
+R LX 1918 o - Ap M>=15 2s 1 S
+R LX 1918 o - S M>=15 2s 0 -
+R LX 1919 o - Mar 1 23 1 S
+R LX 1919 o - O 5 3 0 -
+R LX 1920 o - F 14 23 1 S
+R LX 1920 o - O 24 2 0 -
+R LX 1921 o - Mar 14 23 1 S
+R LX 1921 o - O 26 2 0 -
+R LX 1922 o - Mar 25 23 1 S
+R LX 1922 o - O Su>=2 1 0 -
+R LX 1923 o - Ap 21 23 1 S
+R LX 1923 o - O Su>=2 2 0 -
+R LX 1924 o - Mar 29 23 1 S
+R LX 1924 1928 - O Su>=2 1 0 -
+R LX 1925 o - Ap 5 23 1 S
+R LX 1926 o - Ap 17 23 1 S
+R LX 1927 o - Ap 9 23 1 S
+R LX 1928 o - Ap 14 23 1 S
+R LX 1929 o - Ap 20 23 1 S
+Z Europe/Luxembourg 0:24:36 - LMT 1904 Jun
+1 LX CE%sT 1918 N 25
+0 LX WE%sT 1929 O 6 2s
+0 b WE%sT 1940 May 14 3
+1 c WE%sT 1944 S 18 3
+1 b CE%sT 1977
+1 E CE%sT
+R MT 1973 o - Mar 31 0s 1 S
+R MT 1973 o - S 29 0s 0 -
+R MT 1974 o - Ap 21 0s 1 S
+R MT 1974 o - S 16 0s 0 -
+R MT 1975 1979 - Ap Su>=15 2 1 S
+R MT 1975 1980 - S Su>=15 2 0 -
+R MT 1980 o - Mar 31 2 1 S
+Z Europe/Malta 0:58:4 - LMT 1893 N 2 0s
+1 I CE%sT 1973 Mar 31
+1 MT CE%sT 1981
+1 E CE%sT
+R MD 1997 ma - Mar lastSu 2 1 S
+R MD 1997 ma - O lastSu 3 0 -
+Z Europe/Chisinau 1:55:20 - LMT 1880
+1:55 - CMT 1918 F 15
+1:44:24 - BMT 1931 Jul 24
+2 z EE%sT 1940 Au 15
+2 1 EEST 1941 Jul 17
+1 c CE%sT 1944 Au 24
+3 R MSK/MSD 1990 May 6 2
+2 R EE%sT 1992
+2 e EE%sT 1997
+2 MD EE%sT
+Z Europe/Monaco 0:29:32 - LMT 1892 Jun
+0:9:21 - PMT 1911 Mar 29
+0 F WE%sT 1945 S 16 3
+1 F CE%sT 1977
+1 E CE%sT
+R N 1916 o - May 1 0 1 NST
+R N 1916 o - O 1 0 0 AMT
+R N 1917 o - Ap 16 2s 1 NST
+R N 1917 o - S 17 2s 0 AMT
+R N 1918 1921 - Ap M>=1 2s 1 NST
+R N 1918 1921 - S lastM 2s 0 AMT
+R N 1922 o - Mar lastSu 2s 1 NST
+R N 1922 1936 - O Su>=2 2s 0 AMT
+R N 1923 o - Jun F>=1 2s 1 NST
+R N 1924 o - Mar lastSu 2s 1 NST
+R N 1925 o - Jun F>=1 2s 1 NST
+R N 1926 1931 - May 15 2s 1 NST
+R N 1932 o - May 22 2s 1 NST
+R N 1933 1936 - May 15 2s 1 NST
+R N 1937 o - May 22 2s 1 NST
+R N 1937 o - Jul 1 0 1 S
+R N 1937 1939 - O Su>=2 2s 0 -
+R N 1938 1939 - May 15 2s 1 S
+R N 1945 o - Ap 2 2s 1 S
+R N 1945 o - S 16 2s 0 -
+Z Europe/Amsterdam 0:19:32 - LMT 1835
+0:19:32 N %s 1937 Jul
+0:20 N +0020/+0120 1940 May 16
+1 c CE%sT 1945 Ap 2 2
+1 N CE%sT 1977
+1 E CE%sT
+R NO 1916 o - May 22 1 1 S
+R NO 1916 o - S 30 0 0 -
+R NO 1945 o - Ap 2 2s 1 S
+R NO 1945 o - O 1 2s 0 -
+R NO 1959 1964 - Mar Su>=15 2s 1 S
+R NO 1959 1965 - S Su>=15 2s 0 -
+R NO 1965 o - Ap 25 2s 1 S
+Z Europe/Oslo 0:43 - LMT 1895
+1 NO CE%sT 1940 Au 10 23
+1 c CE%sT 1945 Ap 2 2
+1 NO CE%sT 1980
+1 E CE%sT
+L Europe/Oslo Arctic/Longyearbyen
+R O 1918 1919 - S 16 2s 0 -
+R O 1919 o - Ap 15 2s 1 S
+R O 1944 o - Ap 3 2s 1 S
+R O 1944 o - O 4 2 0 -
+R O 1945 o - Ap 29 0 1 S
+R O 1945 o - N 1 0 0 -
+R O 1946 o - Ap 14 0s 1 S
+R O 1946 o - O 7 2s 0 -
+R O 1947 o - May 4 2s 1 S
+R O 1947 1949 - O Su>=1 2s 0 -
+R O 1948 o - Ap 18 2s 1 S
+R O 1949 o - Ap 10 2s 1 S
+R O 1957 o - Jun 2 1s 1 S
+R O 1957 1958 - S lastSu 1s 0 -
+R O 1958 o - Mar 30 1s 1 S
+R O 1959 o - May 31 1s 1 S
+R O 1959 1961 - O Su>=1 1s 0 -
+R O 1960 o - Ap 3 1s 1 S
+R O 1961 1964 - May lastSu 1s 1 S
+R O 1962 1964 - S lastSu 1s 0 -
+Z Europe/Warsaw 1:24 - LMT 1880
+1:24 - WMT 1915 Au 5
+1 c CE%sT 1918 S 16 3
+2 O EE%sT 1922 Jun
+1 O CE%sT 1940 Jun 23 2
+1 c CE%sT 1944 O
+1 O CE%sT 1977
+1 W- CE%sT 1988
+1 E CE%sT
+R p 1916 o - Jun 17 23 1 S
+R p 1916 o - N 1 1 0 -
+R p 1917 o - F 28 23s 1 S
+R p 1917 1921 - O 14 23s 0 -
+R p 1918 o - Mar 1 23s 1 S
+R p 1919 o - F 28 23s 1 S
+R p 1920 o - F 29 23s 1 S
+R p 1921 o - F 28 23s 1 S
+R p 1924 o - Ap 16 23s 1 S
+R p 1924 o - O 14 23s 0 -
+R p 1926 o - Ap 17 23s 1 S
+R p 1926 1929 - O Sa>=1 23s 0 -
+R p 1927 o - Ap 9 23s 1 S
+R p 1928 o - Ap 14 23s 1 S
+R p 1929 o - Ap 20 23s 1 S
+R p 1931 o - Ap 18 23s 1 S
+R p 1931 1932 - O Sa>=1 23s 0 -
+R p 1932 o - Ap 2 23s 1 S
+R p 1934 o - Ap 7 23s 1 S
+R p 1934 1938 - O Sa>=1 23s 0 -
+R p 1935 o - Mar 30 23s 1 S
+R p 1936 o - Ap 18 23s 1 S
+R p 1937 o - Ap 3 23s 1 S
+R p 1938 o - Mar 26 23s 1 S
+R p 1939 o - Ap 15 23s 1 S
+R p 1939 o - N 18 23s 0 -
+R p 1940 o - F 24 23s 1 S
+R p 1940 1941 - O 5 23s 0 -
+R p 1941 o - Ap 5 23s 1 S
+R p 1942 1945 - Mar Sa>=8 23s 1 S
+R p 1942 o - Ap 25 22s 2 M
+R p 1942 o - Au 15 22s 1 S
+R p 1942 1945 - O Sa>=24 23s 0 -
+R p 1943 o - Ap 17 22s 2 M
+R p 1943 1945 - Au Sa>=25 22s 1 S
+R p 1944 1945 - Ap Sa>=21 22s 2 M
+R p 1946 o - Ap Sa>=1 23s 1 S
+R p 1946 o - O Sa>=1 23s 0 -
+R p 1947 1965 - Ap Su>=1 2s 1 S
+R p 1947 1965 - O Su>=1 2s 0 -
+R p 1977 o - Mar 27 0s 1 S
+R p 1977 o - S 25 0s 0 -
+R p 1978 1979 - Ap Su>=1 0s 1 S
+R p 1978 o - O 1 0s 0 -
+R p 1979 1982 - S lastSu 1s 0 -
+R p 1980 o - Mar lastSu 0s 1 S
+R p 1981 1982 - Mar lastSu 1s 1 S
+R p 1983 o - Mar lastSu 2s 1 S
+Z Europe/Lisbon -0:36:45 - LMT 1884
+-0:36:45 - LMT 1912 Ja 1 0u
+0 p WE%sT 1966 Ap 3 2
+1 - CET 1976 S 26 1
+0 p WE%sT 1983 S 25 1s
+0 W- WE%sT 1992 S 27 1s
+1 E CE%sT 1996 Mar 31 1u
+0 E WE%sT
+Z Atlantic/Azores -1:42:40 - LMT 1884
+-1:54:32 - HMT 1912 Ja 1 2u
+-2 p -02/-01 1942 Ap 25 22s
+-2 p +00 1942 Au 15 22s
+-2 p -02/-01 1943 Ap 17 22s
+-2 p +00 1943 Au 28 22s
+-2 p -02/-01 1944 Ap 22 22s
+-2 p +00 1944 Au 26 22s
+-2 p -02/-01 1945 Ap 21 22s
+-2 p +00 1945 Au 25 22s
+-2 p -02/-01 1966 Ap 3 2
+-1 p -01/+00 1983 S 25 1s
+-1 W- -01/+00 1992 S 27 1s
+0 E WE%sT 1993 Mar 28 1u
+-1 E -01/+00
+Z Atlantic/Madeira -1:7:36 - LMT 1884
+-1:7:36 - FMT 1912 Ja 1 1u
+-1 p -01/+00 1942 Ap 25 22s
+-1 p +01 1942 Au 15 22s
+-1 p -01/+00 1943 Ap 17 22s
+-1 p +01 1943 Au 28 22s
+-1 p -01/+00 1944 Ap 22 22s
+-1 p +01 1944 Au 26 22s
+-1 p -01/+00 1945 Ap 21 22s
+-1 p +01 1945 Au 25 22s
+-1 p -01/+00 1966 Ap 3 2
+0 p WE%sT 1983 S 25 1s
+0 E WE%sT
+R z 1932 o - May 21 0s 1 S
+R z 1932 1939 - O Su>=1 0s 0 -
+R z 1933 1939 - Ap Su>=2 0s 1 S
+R z 1979 o - May 27 0 1 S
+R z 1979 o - S lastSu 0 0 -
+R z 1980 o - Ap 5 23 1 S
+R z 1980 o - S lastSu 1 0 -
+R z 1991 1993 - Mar lastSu 0s 1 S
+R z 1991 1993 - S lastSu 0s 0 -
+Z Europe/Bucharest 1:44:24 - LMT 1891 O
+1:44:24 - BMT 1931 Jul 24
+2 z EE%sT 1981 Mar 29 2s
+2 c EE%sT 1991
+2 z EE%sT 1994
+2 e EE%sT 1997
+2 E EE%sT
+Z Europe/Kaliningrad 1:22 - LMT 1893 Ap
+1 c CE%sT 1945 Ap 10
+2 O EE%sT 1946 Ap 7
+3 R MSK/MSD 1989 Mar 26 2s
+2 R EE%sT 2011 Mar 27 2s
+3 - +03 2014 O 26 2s
+2 - EET
+Z Europe/Moscow 2:30:17 - LMT 1880
+2:30:17 - MMT 1916 Jul 3
+2:31:19 R %s 1919 Jul 1 0u
+3 R %s 1921 O
+3 R MSK/MSD 1922 O
+2 - EET 1930 Jun 21
+3 R MSK/MSD 1991 Mar 31 2s
+2 R EE%sT 1992 Ja 19 2s
+3 R MSK/MSD 2011 Mar 27 2s
+4 - MSK 2014 O 26 2s
+3 - MSK
+Z Europe/Simferopol 2:16:24 - LMT 1880
+2:16 - SMT 1924 May 2
+2 - EET 1930 Jun 21
+3 - MSK 1941 N
+1 c CE%sT 1944 Ap 13
+3 R MSK/MSD 1990
+3 - MSK 1990 Jul 1 2
+2 - EET 1992 Mar 20
+2 c EE%sT 1994 May
+3 e MSK/MSD 1996 Mar 31 0s
+3 1 MSD 1996 O 27 3s
+3 R MSK/MSD 1997
+3 - MSK 1997 Mar lastSu 1u
+2 E EE%sT 2014 Mar 30 2
+4 - MSK 2014 O 26 2s
+3 - MSK
+Z Europe/Astrakhan 3:12:12 - LMT 1924 May
+3 - +03 1930 Jun 21
+4 R +04/+05 1989 Mar 26 2s
+3 R +03/+04 1991 Mar 31 2s
+4 - +04 1992 Mar 29 2s
+3 R +03/+04 2011 Mar 27 2s
+4 - +04 2014 O 26 2s
+3 - +03 2016 Mar 27 2s
+4 - +04
+Z Europe/Volgograd 2:57:40 - LMT 1920 Ja 3
+3 - +03 1930 Jun 21
+4 - +04 1961 N 11
+4 R +04/+05 1988 Mar 27 2s
+3 R +03/+04 1991 Mar 31 2s
+4 - +04 1992 Mar 29 2s
+3 R +03/+04 2011 Mar 27 2s
+4 - +04 2014 O 26 2s
+3 - +03 2018 O 28 2s
+4 - +04 2020 D 27 2s
+3 - +03
+Z Europe/Saratov 3:4:18 - LMT 1919 Jul 1 0u
+3 - +03 1930 Jun 21
+4 R +04/+05 1988 Mar 27 2s
+3 R +03/+04 1991 Mar 31 2s
+4 - +04 1992 Mar 29 2s
+3 R +03/+04 2011 Mar 27 2s
+4 - +04 2014 O 26 2s
+3 - +03 2016 D 4 2s
+4 - +04
+Z Europe/Kirov 3:18:48 - LMT 1919 Jul 1 0u
+3 - +03 1930 Jun 21
+4 R +04/+05 1989 Mar 26 2s
+3 R +03/+04 1991 Mar 31 2s
+4 - +04 1992 Mar 29 2s
+3 R +03/+04 2011 Mar 27 2s
+4 - +04 2014 O 26 2s
+3 - +03
+Z Europe/Samara 3:20:20 - LMT 1919 Jul 1 0u
+3 - +03 1930 Jun 21
+4 - +04 1935 Ja 27
+4 R +04/+05 1989 Mar 26 2s
+3 R +03/+04 1991 Mar 31 2s
+2 R +02/+03 1991 S 29 2s
+3 - +03 1991 O 20 3
+4 R +04/+05 2010 Mar 28 2s
+3 R +03/+04 2011 Mar 27 2s
+4 - +04
+Z Europe/Ulyanovsk 3:13:36 - LMT 1919 Jul 1 0u
+3 - +03 1930 Jun 21
+4 R +04/+05 1989 Mar 26 2s
+3 R +03/+04 1991 Mar 31 2s
+2 R +02/+03 1992 Ja 19 2s
+3 R +03/+04 2011 Mar 27 2s
+4 - +04 2014 O 26 2s
+3 - +03 2016 Mar 27 2s
+4 - +04
+Z Asia/Yekaterinburg 4:2:33 - LMT 1916 Jul 3
+3:45:5 - PMT 1919 Jul 15 4
+4 - +04 1930 Jun 21
+5 R +05/+06 1991 Mar 31 2s
+4 R +04/+05 1992 Ja 19 2s
+5 R +05/+06 2011 Mar 27 2s
+6 - +06 2014 O 26 2s
+5 - +05
+Z Asia/Omsk 4:53:30 - LMT 1919 N 14
+5 - +05 1930 Jun 21
+6 R +06/+07 1991 Mar 31 2s
+5 R +05/+06 1992 Ja 19 2s
+6 R +06/+07 2011 Mar 27 2s
+7 - +07 2014 O 26 2s
+6 - +06
+Z Asia/Barnaul 5:35 - LMT 1919 D 10
+6 - +06 1930 Jun 21
+7 R +07/+08 1991 Mar 31 2s
+6 R +06/+07 1992 Ja 19 2s
+7 R +07/+08 1995 May 28
+6 R +06/+07 2011 Mar 27 2s
+7 - +07 2014 O 26 2s
+6 - +06 2016 Mar 27 2s
+7 - +07
+Z Asia/Novosibirsk 5:31:40 - LMT 1919 D 14 6
+6 - +06 1930 Jun 21
+7 R +07/+08 1991 Mar 31 2s
+6 R +06/+07 1992 Ja 19 2s
+7 R +07/+08 1993 May 23
+6 R +06/+07 2011 Mar 27 2s
+7 - +07 2014 O 26 2s
+6 - +06 2016 Jul 24 2s
+7 - +07
+Z Asia/Tomsk 5:39:51 - LMT 1919 D 22
+6 - +06 1930 Jun 21
+7 R +07/+08 1991 Mar 31 2s
+6 R +06/+07 1992 Ja 19 2s
+7 R +07/+08 2002 May 1 3
+6 R +06/+07 2011 Mar 27 2s
+7 - +07 2014 O 26 2s
+6 - +06 2016 May 29 2s
+7 - +07
+Z Asia/Novokuznetsk 5:48:48 - LMT 1924 May
+6 - +06 1930 Jun 21
+7 R +07/+08 1991 Mar 31 2s
+6 R +06/+07 1992 Ja 19 2s
+7 R +07/+08 2010 Mar 28 2s
+6 R +06/+07 2011 Mar 27 2s
+7 - +07
+Z Asia/Krasnoyarsk 6:11:26 - LMT 1920 Ja 6
+6 - +06 1930 Jun 21
+7 R +07/+08 1991 Mar 31 2s
+6 R +06/+07 1992 Ja 19 2s
+7 R +07/+08 2011 Mar 27 2s
+8 - +08 2014 O 26 2s
+7 - +07
+Z Asia/Irkutsk 6:57:5 - LMT 1880
+6:57:5 - IMT 1920 Ja 25
+7 - +07 1930 Jun 21
+8 R +08/+09 1991 Mar 31 2s
+7 R +07/+08 1992 Ja 19 2s
+8 R +08/+09 2011 Mar 27 2s
+9 - +09 2014 O 26 2s
+8 - +08
+Z Asia/Chita 7:33:52 - LMT 1919 D 15
+8 - +08 1930 Jun 21
+9 R +09/+10 1991 Mar 31 2s
+8 R +08/+09 1992 Ja 19 2s
+9 R +09/+10 2011 Mar 27 2s
+10 - +10 2014 O 26 2s
+8 - +08 2016 Mar 27 2
+9 - +09
+Z Asia/Yakutsk 8:38:58 - LMT 1919 D 15
+8 - +08 1930 Jun 21
+9 R +09/+10 1991 Mar 31 2s
+8 R +08/+09 1992 Ja 19 2s
+9 R +09/+10 2011 Mar 27 2s
+10 - +10 2014 O 26 2s
+9 - +09
+Z Asia/Vladivostok 8:47:31 - LMT 1922 N 15
+9 - +09 1930 Jun 21
+10 R +10/+11 1991 Mar 31 2s
+9 R +09/+10 1992 Ja 19 2s
+10 R +10/+11 2011 Mar 27 2s
+11 - +11 2014 O 26 2s
+10 - +10
+Z Asia/Khandyga 9:2:13 - LMT 1919 D 15
+8 - +08 1930 Jun 21
+9 R +09/+10 1991 Mar 31 2s
+8 R +08/+09 1992 Ja 19 2s
+9 R +09/+10 2004
+10 R +10/+11 2011 Mar 27 2s
+11 - +11 2011 S 13 0s
+10 - +10 2014 O 26 2s
+9 - +09
+Z Asia/Sakhalin 9:30:48 - LMT 1905 Au 23
+9 - +09 1945 Au 25
+11 R +11/+12 1991 Mar 31 2s
+10 R +10/+11 1992 Ja 19 2s
+11 R +11/+12 1997 Mar lastSu 2s
+10 R +10/+11 2011 Mar 27 2s
+11 - +11 2014 O 26 2s
+10 - +10 2016 Mar 27 2s
+11 - +11
+Z Asia/Magadan 10:3:12 - LMT 1924 May 2
+10 - +10 1930 Jun 21
+11 R +11/+12 1991 Mar 31 2s
+10 R +10/+11 1992 Ja 19 2s
+11 R +11/+12 2011 Mar 27 2s
+12 - +12 2014 O 26 2s
+10 - +10 2016 Ap 24 2s
+11 - +11
+Z Asia/Srednekolymsk 10:14:52 - LMT 1924 May 2
+10 - +10 1930 Jun 21
+11 R +11/+12 1991 Mar 31 2s
+10 R +10/+11 1992 Ja 19 2s
+11 R +11/+12 2011 Mar 27 2s
+12 - +12 2014 O 26 2s
+11 - +11
+Z Asia/Ust-Nera 9:32:54 - LMT 1919 D 15
+8 - +08 1930 Jun 21
+9 R +09/+10 1981 Ap
+11 R +11/+12 1991 Mar 31 2s
+10 R +10/+11 1992 Ja 19 2s
+11 R +11/+12 2011 Mar 27 2s
+12 - +12 2011 S 13 0s
+11 - +11 2014 O 26 2s
+10 - +10
+Z Asia/Kamchatka 10:34:36 - LMT 1922 N 10
+11 - +11 1930 Jun 21
+12 R +12/+13 1991 Mar 31 2s
+11 R +11/+12 1992 Ja 19 2s
+12 R +12/+13 2010 Mar 28 2s
+11 R +11/+12 2011 Mar 27 2s
+12 - +12
+Z Asia/Anadyr 11:49:56 - LMT 1924 May 2
+12 - +12 1930 Jun 21
+13 R +13/+14 1982 Ap 1 0s
+12 R +12/+13 1991 Mar 31 2s
+11 R +11/+12 1992 Ja 19 2s
+12 R +12/+13 2010 Mar 28 2s
+11 R +11/+12 2011 Mar 27 2s
+12 - +12
+Z Europe/Belgrade 1:22 - LMT 1884
+1 - CET 1941 Ap 18 23
+1 c CE%sT 1945
+1 - CET 1945 May 8 2s
+1 1 CEST 1945 S 16 2s
+1 - CET 1982 N 27
+1 E CE%sT
+L Europe/Belgrade Europe/Ljubljana
+L Europe/Belgrade Europe/Podgorica
+L Europe/Belgrade Europe/Sarajevo
+L Europe/Belgrade Europe/Skopje
+L Europe/Belgrade Europe/Zagreb
+L Europe/Prague Europe/Bratislava
+R s 1918 o - Ap 15 23 1 S
+R s 1918 1919 - O 6 24s 0 -
+R s 1919 o - Ap 6 23 1 S
+R s 1924 o - Ap 16 23 1 S
+R s 1924 o - O 4 24s 0 -
+R s 1926 o - Ap 17 23 1 S
+R s 1926 1929 - O Sa>=1 24s 0 -
+R s 1927 o - Ap 9 23 1 S
+R s 1928 o - Ap 15 0 1 S
+R s 1929 o - Ap 20 23 1 S
+R s 1937 o - Jun 16 23 1 S
+R s 1937 o - O 2 24s 0 -
+R s 1938 o - Ap 2 23 1 S
+R s 1938 o - Ap 30 23 2 M
+R s 1938 o - O 2 24 1 S
+R s 1939 o - O 7 24s 0 -
+R s 1942 o - May 2 23 1 S
+R s 1942 o - S 1 1 0 -
+R s 1943 1946 - Ap Sa>=13 23 1 S
+R s 1943 1944 - O Su>=1 1 0 -
+R s 1945 1946 - S lastSu 1 0 -
+R s 1949 o - Ap 30 23 1 S
+R s 1949 o - O 2 1 0 -
+R s 1974 1975 - Ap Sa>=12 23 1 S
+R s 1974 1975 - O Su>=1 1 0 -
+R s 1976 o - Mar 27 23 1 S
+R s 1976 1977 - S lastSu 1 0 -
+R s 1977 o - Ap 2 23 1 S
+R s 1978 o - Ap 2 2s 1 S
+R s 1978 o - O 1 2s 0 -
+R Sp 1967 o - Jun 3 12 1 S
+R Sp 1967 o - O 1 0 0 -
+R Sp 1974 o - Jun 24 0 1 S
+R Sp 1974 o - S 1 0 0 -
+R Sp 1976 1977 - May 1 0 1 S
+R Sp 1976 o - Au 1 0 0 -
+R Sp 1977 o - S 28 0 0 -
+R Sp 1978 o - Jun 1 0 1 S
+R Sp 1978 o - Au 4 0 0 -
+Z Europe/Madrid -0:14:44 - LMT 1900 D 31 23:45:16
+0 s WE%sT 1940 Mar 16 23
+1 s CE%sT 1979
+1 E CE%sT
+Z Africa/Ceuta -0:21:16 - LMT 1900 D 31 23:38:44
+0 - WET 1918 May 6 23
+0 1 WEST 1918 O 7 23
+0 - WET 1924
+0 s WE%sT 1929
+0 - WET 1967
+0 Sp WE%sT 1984 Mar 16
+1 - CET 1986
+1 E CE%sT
+Z Atlantic/Canary -1:1:36 - LMT 1922 Mar
+-1 - -01 1946 S 30 1
+0 - WET 1980 Ap 6 0s
+0 1 WEST 1980 S 28 1u
+0 E WE%sT
+Z Europe/Stockholm 1:12:12 - LMT 1879
+1:0:14 - SET 1900
+1 - CET 1916 May 14 23
+1 1 CEST 1916 O 1 1
+1 - CET 1980
+1 E CE%sT
+R CH 1941 1942 - May M>=1 1 1 S
+R CH 1941 1942 - O M>=1 2 0 -
+Z Europe/Zurich 0:34:8 - LMT 1853 Jul 16
+0:29:46 - BMT 1894 Jun
+1 CH CE%sT 1981
+1 E CE%sT
+R T 1916 o - May 1 0 1 S
+R T 1916 o - O 1 0 0 -
+R T 1920 o - Mar 28 0 1 S
+R T 1920 o - O 25 0 0 -
+R T 1921 o - Ap 3 0 1 S
+R T 1921 o - O 3 0 0 -
+R T 1922 o - Mar 26 0 1 S
+R T 1922 o - O 8 0 0 -
+R T 1924 o - May 13 0 1 S
+R T 1924 1925 - O 1 0 0 -
+R T 1925 o - May 1 0 1 S
+R T 1940 o - Jul 1 0 1 S
+R T 1940 o - O 6 0 0 -
+R T 1940 o - D 1 0 1 S
+R T 1941 o - S 21 0 0 -
+R T 1942 o - Ap 1 0 1 S
+R T 1945 o - O 8 0 0 -
+R T 1946 o - Jun 1 0 1 S
+R T 1946 o - O 1 0 0 -
+R T 1947 1948 - Ap Su>=16 0 1 S
+R T 1947 1951 - O Su>=2 0 0 -
+R T 1949 o - Ap 10 0 1 S
+R T 1950 o - Ap 16 0 1 S
+R T 1951 o - Ap 22 0 1 S
+R T 1962 o - Jul 15 0 1 S
+R T 1963 o - O 30 0 0 -
+R T 1964 o - May 15 0 1 S
+R T 1964 o - O 1 0 0 -
+R T 1973 o - Jun 3 1 1 S
+R T 1973 1976 - O Su>=31 2 0 -
+R T 1974 o - Mar 31 2 1 S
+R T 1975 o - Mar 22 2 1 S
+R T 1976 o - Mar 21 2 1 S
+R T 1977 1978 - Ap Su>=1 2 1 S
+R T 1977 1978 - O Su>=15 2 0 -
+R T 1978 o - Jun 29 0 0 -
+R T 1983 o - Jul 31 2 1 S
+R T 1983 o - O 2 2 0 -
+R T 1985 o - Ap 20 1s 1 S
+R T 1985 o - S 28 1s 0 -
+R T 1986 1993 - Mar lastSu 1s 1 S
+R T 1986 1995 - S lastSu 1s 0 -
+R T 1994 o - Mar 20 1s 1 S
+R T 1995 2006 - Mar lastSu 1s 1 S
+R T 1996 2006 - O lastSu 1s 0 -
+Z Europe/Istanbul 1:55:52 - LMT 1880
+1:56:56 - IMT 1910 O
+2 T EE%sT 1978 Jun 29
+3 T +03/+04 1984 N 1 2
+2 T EE%sT 2007
+2 E EE%sT 2011 Mar 27 1u
+2 - EET 2011 Mar 28 1u
+2 E EE%sT 2014 Mar 30 1u
+2 - EET 2014 Mar 31 1u
+2 E EE%sT 2015 O 25 1u
+2 1 EEST 2015 N 8 1u
+2 E EE%sT 2016 S 7
+3 - +03
+L Europe/Istanbul Asia/Istanbul
+Z Europe/Kiev 2:2:4 - LMT 1880
+2:2:4 - KMT 1924 May 2
+2 - EET 1930 Jun 21
+3 - MSK 1941 S 20
+1 c CE%sT 1943 N 6
+3 R MSK/MSD 1990 Jul 1 2
+2 1 EEST 1991 S 29 3
+2 c EE%sT 1996 May 13
+2 E EE%sT
+Z Europe/Uzhgorod 1:29:12 - LMT 1890 O
+1 - CET 1940
+1 c CE%sT 1944 O
+1 1 CEST 1944 O 26
+1 - CET 1945 Jun 29
+3 R MSK/MSD 1990
+3 - MSK 1990 Jul 1 2
+1 - CET 1991 Mar 31 3
+2 - EET 1992 Mar 20
+2 c EE%sT 1996 May 13
+2 E EE%sT
+Z Europe/Zaporozhye 2:20:40 - LMT 1880
+2:20 - +0220 1924 May 2
+2 - EET 1930 Jun 21
+3 - MSK 1941 Au 25
+1 c CE%sT 1943 O 25
+3 R MSK/MSD 1991 Mar 31 2
+2 e EE%sT 1992 Mar 20
+2 c EE%sT 1996 May 13
+2 E EE%sT
+R u 1918 1919 - Mar lastSu 2 1 D
+R u 1918 1919 - O lastSu 2 0 S
+R u 1942 o - F 9 2 1 W
+R u 1945 o - Au 14 23u 1 P
+R u 1945 o - S 30 2 0 S
+R u 1967 2006 - O lastSu 2 0 S
+R u 1967 1973 - Ap lastSu 2 1 D
+R u 1974 o - Ja 6 2 1 D
+R u 1975 o - F lastSu 2 1 D
+R u 1976 1986 - Ap lastSu 2 1 D
+R u 1987 2006 - Ap Su>=1 2 1 D
+R u 2007 ma - Mar Su>=8 2 1 D
+R u 2007 ma - N Su>=1 2 0 S
+Z EST -5 - EST
+Z MST -7 - MST
+Z HST -10 - HST
+Z EST5EDT -5 u E%sT
+Z CST6CDT -6 u C%sT
+Z MST7MDT -7 u M%sT
+Z PST8PDT -8 u P%sT
+R NY 1920 o - Mar lastSu 2 1 D
+R NY 1920 o - O lastSu 2 0 S
+R NY 1921 1966 - Ap lastSu 2 1 D
+R NY 1921 1954 - S lastSu 2 0 S
+R NY 1955 1966 - O lastSu 2 0 S
+Z America/New_York -4:56:2 - LMT 1883 N 18 12:3:58
+-5 u E%sT 1920
+-5 NY E%sT 1942
+-5 u E%sT 1946
+-5 NY E%sT 1967
+-5 u E%sT
+R Ch 1920 o - Jun 13 2 1 D
+R Ch 1920 1921 - O lastSu 2 0 S
+R Ch 1921 o - Mar lastSu 2 1 D
+R Ch 1922 1966 - Ap lastSu 2 1 D
+R Ch 1922 1954 - S lastSu 2 0 S
+R Ch 1955 1966 - O lastSu 2 0 S
+Z America/Chicago -5:50:36 - LMT 1883 N 18 12:9:24
+-6 u C%sT 1920
+-6 Ch C%sT 1936 Mar 1 2
+-5 - EST 1936 N 15 2
+-6 Ch C%sT 1942
+-6 u C%sT 1946
+-6 Ch C%sT 1967
+-6 u C%sT
+Z America/North_Dakota/Center -6:45:12 - LMT 1883 N 18 12:14:48
+-7 u M%sT 1992 O 25 2
+-6 u C%sT
+Z America/North_Dakota/New_Salem -6:45:39 - LMT 1883 N 18 12:14:21
+-7 u M%sT 2003 O 26 2
+-6 u C%sT
+Z America/North_Dakota/Beulah -6:47:7 - LMT 1883 N 18 12:12:53
+-7 u M%sT 2010 N 7 2
+-6 u C%sT
+R De 1920 1921 - Mar lastSu 2 1 D
+R De 1920 o - O lastSu 2 0 S
+R De 1921 o - May 22 2 0 S
+R De 1965 1966 - Ap lastSu 2 1 D
+R De 1965 1966 - O lastSu 2 0 S
+Z America/Denver -6:59:56 - LMT 1883 N 18 12:0:4
+-7 u M%sT 1920
+-7 De M%sT 1942
+-7 u M%sT 1946
+-7 De M%sT 1967
+-7 u M%sT
+R CA 1948 o - Mar 14 2:1 1 D
+R CA 1949 o - Ja 1 2 0 S
+R CA 1950 1966 - Ap lastSu 1 1 D
+R CA 1950 1961 - S lastSu 2 0 S
+R CA 1962 1966 - O lastSu 2 0 S
+Z America/Los_Angeles -7:52:58 - LMT 1883 N 18 12:7:2
+-8 u P%sT 1946
+-8 CA P%sT 1967
+-8 u P%sT
+Z America/Juneau 15:2:19 - LMT 1867 O 19 15:33:32
+-8:57:41 - LMT 1900 Au 20 12
+-8 - PST 1942
+-8 u P%sT 1946
+-8 - PST 1969
+-8 u P%sT 1980 Ap 27 2
+-9 u Y%sT 1980 O 26 2
+-8 u P%sT 1983 O 30 2
+-9 u Y%sT 1983 N 30
+-9 u AK%sT
+Z America/Sitka 14:58:47 - LMT 1867 O 19 15:30
+-9:1:13 - LMT 1900 Au 20 12
+-8 - PST 1942
+-8 u P%sT 1946
+-8 - PST 1969
+-8 u P%sT 1983 O 30 2
+-9 u Y%sT 1983 N 30
+-9 u AK%sT
+Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55
+-8:46:18 - LMT 1900 Au 20 12
+-8 - PST 1942
+-8 u P%sT 1946
+-8 - PST 1969
+-8 u P%sT 1983 O 30 2
+-8 - PST 2015 N 1 2
+-9 u AK%sT 2018 N 4 2
+-8 - PST 2019 Ja 20 2
+-9 u AK%sT
+Z America/Yakutat 14:41:5 - LMT 1867 O 19 15:12:18
+-9:18:55 - LMT 1900 Au 20 12
+-9 - YST 1942
+-9 u Y%sT 1946
+-9 - YST 1969
+-9 u Y%sT 1983 N 30
+-9 u AK%sT
+Z America/Anchorage 14:0:24 - LMT 1867 O 19 14:31:37
+-9:59:36 - LMT 1900 Au 20 12
+-10 - AST 1942
+-10 u A%sT 1967 Ap
+-10 - AHST 1969
+-10 u AH%sT 1983 O 30 2
+-9 u Y%sT 1983 N 30
+-9 u AK%sT
+Z America/Nome 12:58:22 - LMT 1867 O 19 13:29:35
+-11:1:38 - LMT 1900 Au 20 12
+-11 - NST 1942
+-11 u N%sT 1946
+-11 - NST 1967 Ap
+-11 - BST 1969
+-11 u B%sT 1983 O 30 2
+-9 u Y%sT 1983 N 30
+-9 u AK%sT
+Z America/Adak 12:13:22 - LMT 1867 O 19 12:44:35
+-11:46:38 - LMT 1900 Au 20 12
+-11 - NST 1942
+-11 u N%sT 1946
+-11 - NST 1967 Ap
+-11 - BST 1969
+-11 u B%sT 1983 O 30 2
+-10 u AH%sT 1983 N 30
+-10 u H%sT
+Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12
+-10:30 - HST 1933 Ap 30 2
+-10:30 1 HDT 1933 May 21 12
+-10:30 u H%sT 1947 Jun 8 2
+-10 - HST
+Z America/Phoenix -7:28:18 - LMT 1883 N 18 11:31:42
+-7 u M%sT 1944 Ja 1 0:1
+-7 - MST 1944 Ap 1 0:1
+-7 u M%sT 1944 O 1 0:1
+-7 - MST 1967
+-7 u M%sT 1968 Mar 21
+-7 - MST
+L America/Phoenix America/Creston
+Z America/Boise -7:44:49 - LMT 1883 N 18 12:15:11
+-8 u P%sT 1923 May 13 2
+-7 u M%sT 1974
+-7 - MST 1974 F 3 2
+-7 u M%sT
+R In 1941 o - Jun 22 2 1 D
+R In 1941 1954 - S lastSu 2 0 S
+R In 1946 1954 - Ap lastSu 2 1 D
+Z America/Indiana/Indianapolis -5:44:38 - LMT 1883 N 18 12:15:22
+-6 u C%sT 1920
+-6 In C%sT 1942
+-6 u C%sT 1946
+-6 In C%sT 1955 Ap 24 2
+-5 - EST 1957 S 29 2
+-6 - CST 1958 Ap 27 2
+-5 - EST 1969
+-5 u E%sT 1971
+-5 - EST 2006
+-5 u E%sT
+R Ma 1951 o - Ap lastSu 2 1 D
+R Ma 1951 o - S lastSu 2 0 S
+R Ma 1954 1960 - Ap lastSu 2 1 D
+R Ma 1954 1960 - S lastSu 2 0 S
+Z America/Indiana/Marengo -5:45:23 - LMT 1883 N 18 12:14:37
+-6 u C%sT 1951
+-6 Ma C%sT 1961 Ap 30 2
+-5 - EST 1969
+-5 u E%sT 1974 Ja 6 2
+-6 1 CDT 1974 O 27 2
+-5 u E%sT 1976
+-5 - EST 2006
+-5 u E%sT
+R V 1946 o - Ap lastSu 2 1 D
+R V 1946 o - S lastSu 2 0 S
+R V 1953 1954 - Ap lastSu 2 1 D
+R V 1953 1959 - S lastSu 2 0 S
+R V 1955 o - May 1 0 1 D
+R V 1956 1963 - Ap lastSu 2 1 D
+R V 1960 o - O lastSu 2 0 S
+R V 1961 o - S lastSu 2 0 S
+R V 1962 1963 - O lastSu 2 0 S
+Z America/Indiana/Vincennes -5:50:7 - LMT 1883 N 18 12:9:53
+-6 u C%sT 1946
+-6 V C%sT 1964 Ap 26 2
+-5 - EST 1969
+-5 u E%sT 1971
+-5 - EST 2006 Ap 2 2
+-6 u C%sT 2007 N 4 2
+-5 u E%sT
+R Pe 1955 o - May 1 0 1 D
+R Pe 1955 1960 - S lastSu 2 0 S
+R Pe 1956 1963 - Ap lastSu 2 1 D
+R Pe 1961 1963 - O lastSu 2 0 S
+Z America/Indiana/Tell_City -5:47:3 - LMT 1883 N 18 12:12:57
+-6 u C%sT 1946
+-6 Pe C%sT 1964 Ap 26 2
+-5 - EST 1967 O 29 2
+-6 u C%sT 1969 Ap 27 2
+-5 u E%sT 1971
+-5 - EST 2006 Ap 2 2
+-6 u C%sT
+R Pi 1955 o - May 1 0 1 D
+R Pi 1955 1960 - S lastSu 2 0 S
+R Pi 1956 1964 - Ap lastSu 2 1 D
+R Pi 1961 1964 - O lastSu 2 0 S
+Z America/Indiana/Petersburg -5:49:7 - LMT 1883 N 18 12:10:53
+-6 u C%sT 1955
+-6 Pi C%sT 1965 Ap 25 2
+-5 - EST 1966 O 30 2
+-6 u C%sT 1977 O 30 2
+-5 - EST 2006 Ap 2 2
+-6 u C%sT 2007 N 4 2
+-5 u E%sT
+R St 1947 1961 - Ap lastSu 2 1 D
+R St 1947 1954 - S lastSu 2 0 S
+R St 1955 1956 - O lastSu 2 0 S
+R St 1957 1958 - S lastSu 2 0 S
+R St 1959 1961 - O lastSu 2 0 S
+Z America/Indiana/Knox -5:46:30 - LMT 1883 N 18 12:13:30
+-6 u C%sT 1947
+-6 St C%sT 1962 Ap 29 2
+-5 - EST 1963 O 27 2
+-6 u C%sT 1991 O 27 2
+-5 - EST 2006 Ap 2 2
+-6 u C%sT
+R Pu 1946 1960 - Ap lastSu 2 1 D
+R Pu 1946 1954 - S lastSu 2 0 S
+R Pu 1955 1956 - O lastSu 2 0 S
+R Pu 1957 1960 - S lastSu 2 0 S
+Z America/Indiana/Winamac -5:46:25 - LMT 1883 N 18 12:13:35
+-6 u C%sT 1946
+-6 Pu C%sT 1961 Ap 30 2
+-5 - EST 1969
+-5 u E%sT 1971
+-5 - EST 2006 Ap 2 2
+-6 u C%sT 2007 Mar 11 2
+-5 u E%sT
+Z America/Indiana/Vevay -5:40:16 - LMT 1883 N 18 12:19:44
+-6 u C%sT 1954 Ap 25 2
+-5 - EST 1969
+-5 u E%sT 1973
+-5 - EST 2006
+-5 u E%sT
+R v 1921 o - May 1 2 1 D
+R v 1921 o - S 1 2 0 S
+R v 1941 o - Ap lastSu 2 1 D
+R v 1941 o - S lastSu 2 0 S
+R v 1946 o - Ap lastSu 0:1 1 D
+R v 1946 o - Jun 2 2 0 S
+R v 1950 1961 - Ap lastSu 2 1 D
+R v 1950 1955 - S lastSu 2 0 S
+R v 1956 1961 - O lastSu 2 0 S
+Z America/Kentucky/Louisville -5:43:2 - LMT 1883 N 18 12:16:58
+-6 u C%sT 1921
+-6 v C%sT 1942
+-6 u C%sT 1946
+-6 v C%sT 1961 Jul 23 2
+-5 - EST 1968
+-5 u E%sT 1974 Ja 6 2
+-6 1 CDT 1974 O 27 2
+-5 u E%sT
+Z America/Kentucky/Monticello -5:39:24 - LMT 1883 N 18 12:20:36
+-6 u C%sT 1946
+-6 - CST 1968
+-6 u C%sT 2000 O 29 2
+-5 u E%sT
+R Dt 1948 o - Ap lastSu 2 1 D
+R Dt 1948 o - S lastSu 2 0 S
+Z America/Detroit -5:32:11 - LMT 1905
+-6 - CST 1915 May 15 2
+-5 - EST 1942
+-5 u E%sT 1946
+-5 Dt E%sT 1967 Jun 14 0:1
+-5 u E%sT 1969
+-5 - EST 1973
+-5 u E%sT 1975
+-5 - EST 1975 Ap 27 2
+-5 u E%sT
+R Me 1946 o - Ap lastSu 2 1 D
+R Me 1946 o - S lastSu 2 0 S
+R Me 1966 o - Ap lastSu 2 1 D
+R Me 1966 o - O lastSu 2 0 S
+Z America/Menominee -5:50:27 - LMT 1885 S 18 12
+-6 u C%sT 1946
+-6 Me C%sT 1969 Ap 27 2
+-5 - EST 1973 Ap 29 2
+-6 u C%sT
+R C 1918 o - Ap 14 2 1 D
+R C 1918 o - O 27 2 0 S
+R C 1942 o - F 9 2 1 W
+R C 1945 o - Au 14 23u 1 P
+R C 1945 o - S 30 2 0 S
+R C 1974 1986 - Ap lastSu 2 1 D
+R C 1974 2006 - O lastSu 2 0 S
+R C 1987 2006 - Ap Su>=1 2 1 D
+R C 2007 ma - Mar Su>=8 2 1 D
+R C 2007 ma - N Su>=1 2 0 S
+R j 1917 o - Ap 8 2 1 D
+R j 1917 o - S 17 2 0 S
+R j 1919 o - May 5 23 1 D
+R j 1919 o - Au 12 23 0 S
+R j 1920 1935 - May Su>=1 23 1 D
+R j 1920 1935 - O lastSu 23 0 S
+R j 1936 1941 - May M>=9 0 1 D
+R j 1936 1941 - O M>=2 0 0 S
+R j 1946 1950 - May Su>=8 2 1 D
+R j 1946 1950 - O Su>=2 2 0 S
+R j 1951 1986 - Ap lastSu 2 1 D
+R j 1951 1959 - S lastSu 2 0 S
+R j 1960 1986 - O lastSu 2 0 S
+R j 1987 o - Ap Su>=1 0:1 1 D
+R j 1987 2006 - O lastSu 0:1 0 S
+R j 1988 o - Ap Su>=1 0:1 2 DD
+R j 1989 2006 - Ap Su>=1 0:1 1 D
+R j 2007 2011 - Mar Su>=8 0:1 1 D
+R j 2007 2010 - N Su>=1 0:1 0 S
+Z America/St_Johns -3:30:52 - LMT 1884
+-3:30:52 j N%sT 1918
+-3:30:52 C N%sT 1919
+-3:30:52 j N%sT 1935 Mar 30
+-3:30 j N%sT 1942 May 11
+-3:30 C N%sT 1946
+-3:30 j N%sT 2011 N
+-3:30 C N%sT
+Z America/Goose_Bay -4:1:40 - LMT 1884
+-3:30:52 - NST 1918
+-3:30:52 C N%sT 1919
+-3:30:52 - NST 1935 Mar 30
+-3:30 - NST 1936
+-3:30 j N%sT 1942 May 11
+-3:30 C N%sT 1946
+-3:30 j N%sT 1966 Mar 15 2
+-4 j A%sT 2011 N
+-4 C A%sT
+R H 1916 o - Ap 1 0 1 D
+R H 1916 o - O 1 0 0 S
+R H 1920 o - May 9 0 1 D
+R H 1920 o - Au 29 0 0 S
+R H 1921 o - May 6 0 1 D
+R H 1921 1922 - S 5 0 0 S
+R H 1922 o - Ap 30 0 1 D
+R H 1923 1925 - May Su>=1 0 1 D
+R H 1923 o - S 4 0 0 S
+R H 1924 o - S 15 0 0 S
+R H 1925 o - S 28 0 0 S
+R H 1926 o - May 16 0 1 D
+R H 1926 o - S 13 0 0 S
+R H 1927 o - May 1 0 1 D
+R H 1927 o - S 26 0 0 S
+R H 1928 1931 - May Su>=8 0 1 D
+R H 1928 o - S 9 0 0 S
+R H 1929 o - S 3 0 0 S
+R H 1930 o - S 15 0 0 S
+R H 1931 1932 - S M>=24 0 0 S
+R H 1932 o - May 1 0 1 D
+R H 1933 o - Ap 30 0 1 D
+R H 1933 o - O 2 0 0 S
+R H 1934 o - May 20 0 1 D
+R H 1934 o - S 16 0 0 S
+R H 1935 o - Jun 2 0 1 D
+R H 1935 o - S 30 0 0 S
+R H 1936 o - Jun 1 0 1 D
+R H 1936 o - S 14 0 0 S
+R H 1937 1938 - May Su>=1 0 1 D
+R H 1937 1941 - S M>=24 0 0 S
+R H 1939 o - May 28 0 1 D
+R H 1940 1941 - May Su>=1 0 1 D
+R H 1946 1949 - Ap lastSu 2 1 D
+R H 1946 1949 - S lastSu 2 0 S
+R H 1951 1954 - Ap lastSu 2 1 D
+R H 1951 1954 - S lastSu 2 0 S
+R H 1956 1959 - Ap lastSu 2 1 D
+R H 1956 1959 - S lastSu 2 0 S
+R H 1962 1973 - Ap lastSu 2 1 D
+R H 1962 1973 - O lastSu 2 0 S
+Z America/Halifax -4:14:24 - LMT 1902 Jun 15
+-4 H A%sT 1918
+-4 C A%sT 1919
+-4 H A%sT 1942 F 9 2s
+-4 C A%sT 1946
+-4 H A%sT 1974
+-4 C A%sT
+Z America/Glace_Bay -3:59:48 - LMT 1902 Jun 15
+-4 C A%sT 1953
+-4 H A%sT 1954
+-4 - AST 1972
+-4 H A%sT 1974
+-4 C A%sT
+R o 1933 1935 - Jun Su>=8 1 1 D
+R o 1933 1935 - S Su>=8 1 0 S
+R o 1936 1938 - Jun Su>=1 1 1 D
+R o 1936 1938 - S Su>=1 1 0 S
+R o 1939 o - May 27 1 1 D
+R o 1939 1941 - S Sa>=21 1 0 S
+R o 1940 o - May 19 1 1 D
+R o 1941 o - May 4 1 1 D
+R o 1946 1972 - Ap lastSu 2 1 D
+R o 1946 1956 - S lastSu 2 0 S
+R o 1957 1972 - O lastSu 2 0 S
+R o 1993 2006 - Ap Su>=1 0:1 1 D
+R o 1993 2006 - O lastSu 0:1 0 S
+Z America/Moncton -4:19:8 - LMT 1883 D 9
+-5 - EST 1902 Jun 15
+-4 C A%sT 1933
+-4 o A%sT 1942
+-4 C A%sT 1946
+-4 o A%sT 1973
+-4 C A%sT 1993
+-4 o A%sT 2007
+-4 C A%sT
+R t 1919 o - Mar 30 23:30 1 D
+R t 1919 o - O 26 0 0 S
+R t 1920 o - May 2 2 1 D
+R t 1920 o - S 26 0 0 S
+R t 1921 o - May 15 2 1 D
+R t 1921 o - S 15 2 0 S
+R t 1922 1923 - May Su>=8 2 1 D
+R t 1922 1926 - S Su>=15 2 0 S
+R t 1924 1927 - May Su>=1 2 1 D
+R t 1927 1937 - S Su>=25 2 0 S
+R t 1928 1937 - Ap Su>=25 2 1 D
+R t 1938 1940 - Ap lastSu 2 1 D
+R t 1938 1939 - S lastSu 2 0 S
+R t 1945 1946 - S lastSu 2 0 S
+R t 1946 o - Ap lastSu 2 1 D
+R t 1947 1949 - Ap lastSu 0 1 D
+R t 1947 1948 - S lastSu 0 0 S
+R t 1949 o - N lastSu 0 0 S
+R t 1950 1973 - Ap lastSu 2 1 D
+R t 1950 o - N lastSu 2 0 S
+R t 1951 1956 - S lastSu 2 0 S
+R t 1957 1973 - O lastSu 2 0 S
+Z America/Toronto -5:17:32 - LMT 1895
+-5 C E%sT 1919
+-5 t E%sT 1942 F 9 2s
+-5 C E%sT 1946
+-5 t E%sT 1974
+-5 C E%sT
+L America/Toronto America/Nassau
+Z America/Thunder_Bay -5:57 - LMT 1895
+-6 - CST 1910
+-5 - EST 1942
+-5 C E%sT 1970
+-5 t E%sT 1973
+-5 - EST 1974
+-5 C E%sT
+Z America/Nipigon -5:53:4 - LMT 1895
+-5 C E%sT 1940 S 29
+-5 1 EDT 1942 F 9 2s
+-5 C E%sT
+Z America/Rainy_River -6:18:16 - LMT 1895
+-6 C C%sT 1940 S 29
+-6 1 CDT 1942 F 9 2s
+-6 C C%sT
+R W 1916 o - Ap 23 0 1 D
+R W 1916 o - S 17 0 0 S
+R W 1918 o - Ap 14 2 1 D
+R W 1918 o - O 27 2 0 S
+R W 1937 o - May 16 2 1 D
+R W 1937 o - S 26 2 0 S
+R W 1942 o - F 9 2 1 W
+R W 1945 o - Au 14 23u 1 P
+R W 1945 o - S lastSu 2 0 S
+R W 1946 o - May 12 2 1 D
+R W 1946 o - O 13 2 0 S
+R W 1947 1949 - Ap lastSu 2 1 D
+R W 1947 1949 - S lastSu 2 0 S
+R W 1950 o - May 1 2 1 D
+R W 1950 o - S 30 2 0 S
+R W 1951 1960 - Ap lastSu 2 1 D
+R W 1951 1958 - S lastSu 2 0 S
+R W 1959 o - O lastSu 2 0 S
+R W 1960 o - S lastSu 2 0 S
+R W 1963 o - Ap lastSu 2 1 D
+R W 1963 o - S 22 2 0 S
+R W 1966 1986 - Ap lastSu 2s 1 D
+R W 1966 2005 - O lastSu 2s 0 S
+R W 1987 2005 - Ap Su>=1 2s 1 D
+Z America/Winnipeg -6:28:36 - LMT 1887 Jul 16
+-6 W C%sT 2006
+-6 C C%sT
+R r 1918 o - Ap 14 2 1 D
+R r 1918 o - O 27 2 0 S
+R r 1930 1934 - May Su>=1 0 1 D
+R r 1930 1934 - O Su>=1 0 0 S
+R r 1937 1941 - Ap Su>=8 0 1 D
+R r 1937 o - O Su>=8 0 0 S
+R r 1938 o - O Su>=1 0 0 S
+R r 1939 1941 - O Su>=8 0 0 S
+R r 1942 o - F 9 2 1 W
+R r 1945 o - Au 14 23u 1 P
+R r 1945 o - S lastSu 2 0 S
+R r 1946 o - Ap Su>=8 2 1 D
+R r 1946 o - O Su>=8 2 0 S
+R r 1947 1957 - Ap lastSu 2 1 D
+R r 1947 1957 - S lastSu 2 0 S
+R r 1959 o - Ap lastSu 2 1 D
+R r 1959 o - O lastSu 2 0 S
+R Sw 1957 o - Ap lastSu 2 1 D
+R Sw 1957 o - O lastSu 2 0 S
+R Sw 1959 1961 - Ap lastSu 2 1 D
+R Sw 1959 o - O lastSu 2 0 S
+R Sw 1960 1961 - S lastSu 2 0 S
+Z America/Regina -6:58:36 - LMT 1905 S
+-7 r M%sT 1960 Ap lastSu 2
+-6 - CST
+Z America/Swift_Current -7:11:20 - LMT 1905 S
+-7 C M%sT 1946 Ap lastSu 2
+-7 r M%sT 1950
+-7 Sw M%sT 1972 Ap lastSu 2
+-6 - CST
+R Ed 1918 1919 - Ap Su>=8 2 1 D
+R Ed 1918 o - O 27 2 0 S
+R Ed 1919 o - May 27 2 0 S
+R Ed 1920 1923 - Ap lastSu 2 1 D
+R Ed 1920 o - O lastSu 2 0 S
+R Ed 1921 1923 - S lastSu 2 0 S
+R Ed 1942 o - F 9 2 1 W
+R Ed 1945 o - Au 14 23u 1 P
+R Ed 1945 o - S lastSu 2 0 S
+R Ed 1947 o - Ap lastSu 2 1 D
+R Ed 1947 o - S lastSu 2 0 S
+R Ed 1972 1986 - Ap lastSu 2 1 D
+R Ed 1972 2006 - O lastSu 2 0 S
+Z America/Edmonton -7:33:52 - LMT 1906 S
+-7 Ed M%sT 1987
+-7 C M%sT
+R Va 1918 o - Ap 14 2 1 D
+R Va 1918 o - O 27 2 0 S
+R Va 1942 o - F 9 2 1 W
+R Va 1945 o - Au 14 23u 1 P
+R Va 1945 o - S 30 2 0 S
+R Va 1946 1986 - Ap lastSu 2 1 D
+R Va 1946 o - S 29 2 0 S
+R Va 1947 1961 - S lastSu 2 0 S
+R Va 1962 2006 - O lastSu 2 0 S
+Z America/Vancouver -8:12:28 - LMT 1884
+-8 Va P%sT 1987
+-8 C P%sT
+Z America/Dawson_Creek -8:0:56 - LMT 1884
+-8 C P%sT 1947
+-8 Va P%sT 1972 Au 30 2
+-7 - MST
+Z America/Fort_Nelson -8:10:47 - LMT 1884
+-8 Va P%sT 1946
+-8 - PST 1947
+-8 Va P%sT 1987
+-8 C P%sT 2015 Mar 8 2
+-7 - MST
+R Y 1918 o - Ap 14 2 1 D
+R Y 1918 o - O 27 2 0 S
+R Y 1919 o - May 25 2 1 D
+R Y 1919 o - N 1 0 0 S
+R Y 1942 o - F 9 2 1 W
+R Y 1945 o - Au 14 23u 1 P
+R Y 1945 o - S 30 2 0 S
+R Y 1965 o - Ap lastSu 0 2 DD
+R Y 1965 o - O lastSu 2 0 S
+R Y 1980 1986 - Ap lastSu 2 1 D
+R Y 1980 2006 - O lastSu 2 0 S
+R Y 1987 2006 - Ap Su>=1 2 1 D
+Z America/Pangnirtung 0 - -00 1921
+-4 Y A%sT 1995 Ap Su>=1 2
+-5 C E%sT 1999 O 31 2
+-6 C C%sT 2000 O 29 2
+-5 C E%sT
+Z America/Iqaluit 0 - -00 1942 Au
+-5 Y E%sT 1999 O 31 2
+-6 C C%sT 2000 O 29 2
+-5 C E%sT
+Z America/Resolute 0 - -00 1947 Au 31
+-6 Y C%sT 2000 O 29 2
+-5 - EST 2001 Ap 1 3
+-6 C C%sT 2006 O 29 2
+-5 - EST 2007 Mar 11 3
+-6 C C%sT
+Z America/Rankin_Inlet 0 - -00 1957
+-6 Y C%sT 2000 O 29 2
+-5 - EST 2001 Ap 1 3
+-6 C C%sT
+Z America/Cambridge_Bay 0 - -00 1920
+-7 Y M%sT 1999 O 31 2
+-6 C C%sT 2000 O 29 2
+-5 - EST 2000 N 5
+-6 - CST 2001 Ap 1 3
+-7 C M%sT
+Z America/Yellowknife 0 - -00 1935
+-7 Y M%sT 1980
+-7 C M%sT
+Z America/Inuvik 0 - -00 1953
+-8 Y P%sT 1979 Ap lastSu 2
+-7 Y M%sT 1980
+-7 C M%sT
+Z America/Whitehorse -9:0:12 - LMT 1900 Au 20
+-9 Y Y%sT 1967 May 28
+-8 Y P%sT 1980
+-8 C P%sT 2020 N
+-7 - MST
+Z America/Dawson -9:17:40 - LMT 1900 Au 20
+-9 Y Y%sT 1973 O 28
+-8 Y P%sT 1980
+-8 C P%sT 2020 N
+-7 - MST
+R m 1939 o - F 5 0 1 D
+R m 1939 o - Jun 25 0 0 S
+R m 1940 o - D 9 0 1 D
+R m 1941 o - Ap 1 0 0 S
+R m 1943 o - D 16 0 1 W
+R m 1944 o - May 1 0 0 S
+R m 1950 o - F 12 0 1 D
+R m 1950 o - Jul 30 0 0 S
+R m 1996 2000 - Ap Su>=1 2 1 D
+R m 1996 2000 - O lastSu 2 0 S
+R m 2001 o - May Su>=1 2 1 D
+R m 2001 o - S lastSu 2 0 S
+R m 2002 ma - Ap Su>=1 2 1 D
+R m 2002 ma - O lastSu 2 0 S
+Z America/Cancun -5:47:4 - LMT 1922 Ja 1 0:12:56
+-6 - CST 1981 D 23
+-5 m E%sT 1998 Au 2 2
+-6 m C%sT 2015 F 1 2
+-5 - EST
+Z America/Merida -5:58:28 - LMT 1922 Ja 1 0:1:32
+-6 - CST 1981 D 23
+-5 - EST 1982 D 2
+-6 m C%sT
+Z America/Matamoros -6:40 - LMT 1921 D 31 23:20
+-6 - CST 1988
+-6 u C%sT 1989
+-6 m C%sT 2010
+-6 u C%sT
+Z America/Monterrey -6:41:16 - LMT 1921 D 31 23:18:44
+-6 - CST 1988
+-6 u C%sT 1989
+-6 m C%sT
+Z America/Mexico_City -6:36:36 - LMT 1922 Ja 1 0:23:24
+-7 - MST 1927 Jun 10 23
+-6 - CST 1930 N 15
+-7 - MST 1931 May 1 23
+-6 - CST 1931 O
+-7 - MST 1932 Ap
+-6 m C%sT 2001 S 30 2
+-6 - CST 2002 F 20
+-6 m C%sT
+Z America/Ojinaga -6:57:40 - LMT 1922 Ja 1 0:2:20
+-7 - MST 1927 Jun 10 23
+-6 - CST 1930 N 15
+-7 - MST 1931 May 1 23
+-6 - CST 1931 O
+-7 - MST 1932 Ap
+-6 - CST 1996
+-6 m C%sT 1998
+-6 - CST 1998 Ap Su>=1 3
+-7 m M%sT 2010
+-7 u M%sT
+Z America/Chihuahua -7:4:20 - LMT 1921 D 31 23:55:40
+-7 - MST 1927 Jun 10 23
+-6 - CST 1930 N 15
+-7 - MST 1931 May 1 23
+-6 - CST 1931 O
+-7 - MST 1932 Ap
+-6 - CST 1996
+-6 m C%sT 1998
+-6 - CST 1998 Ap Su>=1 3
+-7 m M%sT
+Z America/Hermosillo -7:23:52 - LMT 1921 D 31 23:36:8
+-7 - MST 1927 Jun 10 23
+-6 - CST 1930 N 15
+-7 - MST 1931 May 1 23
+-6 - CST 1931 O
+-7 - MST 1932 Ap
+-6 - CST 1942 Ap 24
+-7 - MST 1949 Ja 14
+-8 - PST 1970
+-7 m M%sT 1999
+-7 - MST
+Z America/Mazatlan -7:5:40 - LMT 1921 D 31 23:54:20
+-7 - MST 1927 Jun 10 23
+-6 - CST 1930 N 15
+-7 - MST 1931 May 1 23
+-6 - CST 1931 O
+-7 - MST 1932 Ap
+-6 - CST 1942 Ap 24
+-7 - MST 1949 Ja 14
+-8 - PST 1970
+-7 m M%sT
+Z America/Bahia_Banderas -7:1 - LMT 1921 D 31 23:59
+-7 - MST 1927 Jun 10 23
+-6 - CST 1930 N 15
+-7 - MST 1931 May 1 23
+-6 - CST 1931 O
+-7 - MST 1932 Ap
+-6 - CST 1942 Ap 24
+-7 - MST 1949 Ja 14
+-8 - PST 1970
+-7 m M%sT 2010 Ap 4 2
+-6 m C%sT
+Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 0:11:56
+-7 - MST 1924
+-8 - PST 1927 Jun 10 23
+-7 - MST 1930 N 15
+-8 - PST 1931 Ap
+-8 1 PDT 1931 S 30
+-8 - PST 1942 Ap 24
+-8 1 PWT 1945 Au 14 23u
+-8 1 PPT 1945 N 12
+-8 - PST 1948 Ap 5
+-8 1 PDT 1949 Ja 14
+-8 - PST 1954
+-8 CA P%sT 1961
+-8 - PST 1976
+-8 u P%sT 1996
+-8 m P%sT 2001
+-8 u P%sT 2002 F 20
+-8 m P%sT 2010
+-8 u P%sT
+R BB 1942 o - Ap 19 5u 1 D
+R BB 1942 o - Au 31 6u 0 S
+R BB 1943 o - May 2 5u 1 D
+R BB 1943 o - S 5 6u 0 S
+R BB 1944 o - Ap 10 5u 0:30 -
+R BB 1944 o - S 10 6u 0 S
+R BB 1977 o - Jun 12 2 1 D
+R BB 1977 1978 - O Su>=1 2 0 S
+R BB 1978 1980 - Ap Su>=15 2 1 D
+R BB 1979 o - S 30 2 0 S
+R BB 1980 o - S 25 2 0 S
+Z America/Barbados -3:58:29 - LMT 1911 Au 28
+-4 BB A%sT 1944
+-4 BB AST/-0330 1945
+-4 BB A%sT
+R BZ 1918 1941 - O Sa>=1 24 0:30 -0530
+R BZ 1919 1942 - F Sa>=8 24 0 CST
+R BZ 1942 o - Jun 27 24 1 CWT
+R BZ 1945 o - Au 14 23u 1 CPT
+R BZ 1945 o - D 15 24 0 CST
+R BZ 1947 1967 - O Sa>=1 24 0:30 -0530
+R BZ 1948 1968 - F Sa>=8 24 0 CST
+R BZ 1973 o - D 5 0 1 CDT
+R BZ 1974 o - F 9 0 0 CST
+R BZ 1982 o - D 18 0 1 CDT
+R BZ 1983 o - F 12 0 0 CST
+Z America/Belize -5:52:48 - LMT 1912 Ap
+-6 BZ %s
+R Be 1917 o - Ap 5 24 1 -
+R Be 1917 o - S 30 24 0 -
+R Be 1918 o - Ap 13 24 1 -
+R Be 1918 o - S 15 24 0 S
+R Be 1942 o - Ja 11 2 1 D
+R Be 1942 o - O 18 2 0 S
+R Be 1943 o - Mar 21 2 1 D
+R Be 1943 o - O 31 2 0 S
+R Be 1944 1945 - Mar Su>=8 2 1 D
+R Be 1944 1945 - N Su>=1 2 0 S
+R Be 1947 o - May Su>=15 2 1 D
+R Be 1947 o - S Su>=8 2 0 S
+R Be 1948 1952 - May Su>=22 2 1 D
+R Be 1948 1952 - S Su>=1 2 0 S
+R Be 1956 o - May Su>=22 2 1 D
+R Be 1956 o - O lastSu 2 0 S
+Z Atlantic/Bermuda -4:19:18 - LMT 1890
+-4:19:18 Be BMT/BST 1930 Ja 1 2
+-4 Be A%sT 1974 Ap 28 2
+-4 C A%sT 1976
+-4 u A%sT
+R CR 1979 1980 - F lastSu 0 1 D
+R CR 1979 1980 - Jun Su>=1 0 0 S
+R CR 1991 1992 - Ja Sa>=15 0 1 D
+R CR 1991 o - Jul 1 0 0 S
+R CR 1992 o - Mar 15 0 0 S
+Z America/Costa_Rica -5:36:13 - LMT 1890
+-5:36:13 - SJMT 1921 Ja 15
+-6 CR C%sT
+R Q 1928 o - Jun 10 0 1 D
+R Q 1928 o - O 10 0 0 S
+R Q 1940 1942 - Jun Su>=1 0 1 D
+R Q 1940 1942 - S Su>=1 0 0 S
+R Q 1945 1946 - Jun Su>=1 0 1 D
+R Q 1945 1946 - S Su>=1 0 0 S
+R Q 1965 o - Jun 1 0 1 D
+R Q 1965 o - S 30 0 0 S
+R Q 1966 o - May 29 0 1 D
+R Q 1966 o - O 2 0 0 S
+R Q 1967 o - Ap 8 0 1 D
+R Q 1967 1968 - S Su>=8 0 0 S
+R Q 1968 o - Ap 14 0 1 D
+R Q 1969 1977 - Ap lastSu 0 1 D
+R Q 1969 1971 - O lastSu 0 0 S
+R Q 1972 1974 - O 8 0 0 S
+R Q 1975 1977 - O lastSu 0 0 S
+R Q 1978 o - May 7 0 1 D
+R Q 1978 1990 - O Su>=8 0 0 S
+R Q 1979 1980 - Mar Su>=15 0 1 D
+R Q 1981 1985 - May Su>=5 0 1 D
+R Q 1986 1989 - Mar Su>=14 0 1 D
+R Q 1990 1997 - Ap Su>=1 0 1 D
+R Q 1991 1995 - O Su>=8 0s 0 S
+R Q 1996 o - O 6 0s 0 S
+R Q 1997 o - O 12 0s 0 S
+R Q 1998 1999 - Mar lastSu 0s 1 D
+R Q 1998 2003 - O lastSu 0s 0 S
+R Q 2000 2003 - Ap Su>=1 0s 1 D
+R Q 2004 o - Mar lastSu 0s 1 D
+R Q 2006 2010 - O lastSu 0s 0 S
+R Q 2007 o - Mar Su>=8 0s 1 D
+R Q 2008 o - Mar Su>=15 0s 1 D
+R Q 2009 2010 - Mar Su>=8 0s 1 D
+R Q 2011 o - Mar Su>=15 0s 1 D
+R Q 2011 o - N 13 0s 0 S
+R Q 2012 o - Ap 1 0s 1 D
+R Q 2012 ma - N Su>=1 0s 0 S
+R Q 2013 ma - Mar Su>=8 0s 1 D
+Z America/Havana -5:29:28 - LMT 1890
+-5:29:36 - HMT 1925 Jul 19 12
+-5 Q C%sT
+R DO 1966 o - O 30 0 1 EDT
+R DO 1967 o - F 28 0 0 EST
+R DO 1969 1973 - O lastSu 0 0:30 -0430
+R DO 1970 o - F 21 0 0 EST
+R DO 1971 o - Ja 20 0 0 EST
+R DO 1972 1974 - Ja 21 0 0 EST
+Z America/Santo_Domingo -4:39:36 - LMT 1890
+-4:40 - SDMT 1933 Ap 1 12
+-5 DO %s 1974 O 27
+-4 - AST 2000 O 29 2
+-5 u E%sT 2000 D 3 1
+-4 - AST
+R SV 1987 1988 - May Su>=1 0 1 D
+R SV 1987 1988 - S lastSu 0 0 S
+Z America/El_Salvador -5:56:48 - LMT 1921
+-6 SV C%sT
+R GT 1973 o - N 25 0 1 D
+R GT 1974 o - F 24 0 0 S
+R GT 1983 o - May 21 0 1 D
+R GT 1983 o - S 22 0 0 S
+R GT 1991 o - Mar 23 0 1 D
+R GT 1991 o - S 7 0 0 S
+R GT 2006 o - Ap 30 0 1 D
+R GT 2006 o - O 1 0 0 S
+Z America/Guatemala -6:2:4 - LMT 1918 O 5
+-6 GT C%sT
+R HT 1983 o - May 8 0 1 D
+R HT 1984 1987 - Ap lastSu 0 1 D
+R HT 1983 1987 - O lastSu 0 0 S
+R HT 1988 1997 - Ap Su>=1 1s 1 D
+R HT 1988 1997 - O lastSu 1s 0 S
+R HT 2005 2006 - Ap Su>=1 0 1 D
+R HT 2005 2006 - O lastSu 0 0 S
+R HT 2012 2015 - Mar Su>=8 2 1 D
+R HT 2012 2015 - N Su>=1 2 0 S
+R HT 2017 ma - Mar Su>=8 2 1 D
+R HT 2017 ma - N Su>=1 2 0 S
+Z America/Port-au-Prince -4:49:20 - LMT 1890
+-4:49 - PPMT 1917 Ja 24 12
+-5 HT E%sT
+R HN 1987 1988 - May Su>=1 0 1 D
+R HN 1987 1988 - S lastSu 0 0 S
+R HN 2006 o - May Su>=1 0 1 D
+R HN 2006 o - Au M>=1 0 0 S
+Z America/Tegucigalpa -5:48:52 - LMT 1921 Ap
+-6 HN C%sT
+Z America/Jamaica -5:7:10 - LMT 1890
+-5:7:10 - KMT 1912 F
+-5 - EST 1974
+-5 u E%sT 1984
+-5 - EST
+Z America/Martinique -4:4:20 - LMT 1890
+-4:4:20 - FFMT 1911 May
+-4 - AST 1980 Ap 6
+-4 1 ADT 1980 S 28
+-4 - AST
+R NI 1979 1980 - Mar Su>=16 0 1 D
+R NI 1979 1980 - Jun M>=23 0 0 S
+R NI 2005 o - Ap 10 0 1 D
+R NI 2005 o - O Su>=1 0 0 S
+R NI 2006 o - Ap 30 2 1 D
+R NI 2006 o - O Su>=1 1 0 S
+Z America/Managua -5:45:8 - LMT 1890
+-5:45:12 - MMT 1934 Jun 23
+-6 - CST 1973 May
+-5 - EST 1975 F 16
+-6 NI C%sT 1992 Ja 1 4
+-5 - EST 1992 S 24
+-6 - CST 1993
+-5 - EST 1997
+-6 NI C%sT
+Z America/Panama -5:18:8 - LMT 1890
+-5:19:36 - CMT 1908 Ap 22
+-5 - EST
+L America/Panama America/Atikokan
+L America/Panama America/Cayman
+Z America/Puerto_Rico -4:24:25 - LMT 1899 Mar 28 12
+-4 - AST 1942 May 3
+-4 u A%sT 1946
+-4 - AST
+L America/Puerto_Rico America/Anguilla
+L America/Puerto_Rico America/Antigua
+L America/Puerto_Rico America/Aruba
+L America/Puerto_Rico America/Curacao
+L America/Puerto_Rico America/Blanc-Sablon
+L America/Puerto_Rico America/Dominica
+L America/Puerto_Rico America/Grenada
+L America/Puerto_Rico America/Guadeloupe
+L America/Puerto_Rico America/Kralendijk
+L America/Puerto_Rico America/Lower_Princes
+L America/Puerto_Rico America/Marigot
+L America/Puerto_Rico America/Montserrat
+L America/Puerto_Rico America/Port_of_Spain
+L America/Puerto_Rico America/St_Barthelemy
+L America/Puerto_Rico America/St_Kitts
+L America/Puerto_Rico America/St_Lucia
+L America/Puerto_Rico America/St_Thomas
+L America/Puerto_Rico America/St_Vincent
+L America/Puerto_Rico America/Tortola
+Z America/Miquelon -3:44:40 - LMT 1911 May 15
+-4 - AST 1980 May
+-3 - -03 1987
+-3 C -03/-02
+Z America/Grand_Turk -4:44:32 - LMT 1890
+-5:7:10 - KMT 1912 F
+-5 - EST 1979
+-5 u E%sT 2015 Mar 8 2
+-4 - AST 2018 Mar 11 3
+-5 u E%sT
+R A 1930 o - D 1 0 1 -
+R A 1931 o - Ap 1 0 0 -
+R A 1931 o - O 15 0 1 -
+R A 1932 1940 - Mar 1 0 0 -
+R A 1932 1939 - N 1 0 1 -
+R A 1940 o - Jul 1 0 1 -
+R A 1941 o - Jun 15 0 0 -
+R A 1941 o - O 15 0 1 -
+R A 1943 o - Au 1 0 0 -
+R A 1943 o - O 15 0 1 -
+R A 1946 o - Mar 1 0 0 -
+R A 1946 o - O 1 0 1 -
+R A 1963 o - O 1 0 0 -
+R A 1963 o - D 15 0 1 -
+R A 1964 1966 - Mar 1 0 0 -
+R A 1964 1966 - O 15 0 1 -
+R A 1967 o - Ap 2 0 0 -
+R A 1967 1968 - O Su>=1 0 1 -
+R A 1968 1969 - Ap Su>=1 0 0 -
+R A 1974 o - Ja 23 0 1 -
+R A 1974 o - May 1 0 0 -
+R A 1988 o - D 1 0 1 -
+R A 1989 1993 - Mar Su>=1 0 0 -
+R A 1989 1992 - O Su>=15 0 1 -
+R A 1999 o - O Su>=1 0 1 -
+R A 2000 o - Mar 3 0 0 -
+R A 2007 o - D 30 0 1 -
+R A 2008 2009 - Mar Su>=15 0 0 -
+R A 2008 o - O Su>=15 0 1 -
+Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 A -03/-02
+Z America/Argentina/Cordoba -4:16:48 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1991 Mar 3
+-4 - -04 1991 O 20
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 A -03/-02
+Z America/Argentina/Salta -4:21:40 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1991 Mar 3
+-4 - -04 1991 O 20
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 A -03/-02 2008 O 18
+-3 - -03
+Z America/Argentina/Tucuman -4:20:52 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1991 Mar 3
+-4 - -04 1991 O 20
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 - -03 2004 Jun
+-4 - -04 2004 Jun 13
+-3 A -03/-02
+Z America/Argentina/La_Rioja -4:27:24 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1991 Mar
+-4 - -04 1991 May 7
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 - -03 2004 Jun
+-4 - -04 2004 Jun 20
+-3 A -03/-02 2008 O 18
+-3 - -03
+Z America/Argentina/San_Juan -4:34:4 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1991 Mar
+-4 - -04 1991 May 7
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 - -03 2004 May 31
+-4 - -04 2004 Jul 25
+-3 A -03/-02 2008 O 18
+-3 - -03
+Z America/Argentina/Jujuy -4:21:12 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1990 Mar 4
+-4 - -04 1990 O 28
+-4 1 -03 1991 Mar 17
+-4 - -04 1991 O 6
+-3 1 -02 1992
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 A -03/-02 2008 O 18
+-3 - -03
+Z America/Argentina/Catamarca -4:23:8 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1991 Mar 3
+-4 - -04 1991 O 20
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 - -03 2004 Jun
+-4 - -04 2004 Jun 20
+-3 A -03/-02 2008 O 18
+-3 - -03
+Z America/Argentina/Mendoza -4:35:16 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1990 Mar 4
+-4 - -04 1990 O 15
+-4 1 -03 1991 Mar
+-4 - -04 1991 O 15
+-4 1 -03 1992 Mar
+-4 - -04 1992 O 18
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 - -03 2004 May 23
+-4 - -04 2004 S 26
+-3 A -03/-02 2008 O 18
+-3 - -03
+R Sa 2008 2009 - Mar Su>=8 0 0 -
+R Sa 2007 2008 - O Su>=8 0 1 -
+Z America/Argentina/San_Luis -4:25:24 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1990
+-3 1 -02 1990 Mar 14
+-4 - -04 1990 O 15
+-4 1 -03 1991 Mar
+-4 - -04 1991 Jun
+-3 - -03 1999 O 3
+-4 1 -03 2000 Mar 3
+-3 - -03 2004 May 31
+-4 - -04 2004 Jul 25
+-3 A -03/-02 2008 Ja 21
+-4 Sa -04/-03 2009 O 11
+-3 - -03
+Z America/Argentina/Rio_Gallegos -4:36:52 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 - -03 2004 Jun
+-4 - -04 2004 Jun 20
+-3 A -03/-02 2008 O 18
+-3 - -03
+Z America/Argentina/Ushuaia -4:33:12 - LMT 1894 O 31
+-4:16:48 - CMT 1920 May
+-4 - -04 1930 D
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1999 O 3
+-4 A -04/-03 2000 Mar 3
+-3 - -03 2004 May 30
+-4 - -04 2004 Jun 20
+-3 A -03/-02 2008 O 18
+-3 - -03
+Z America/La_Paz -4:32:36 - LMT 1890
+-4:32:36 - CMT 1931 O 15
+-4:32:36 1 BST 1932 Mar 21
+-4 - -04
+R B 1931 o - O 3 11 1 -
+R B 1932 1933 - Ap 1 0 0 -
+R B 1932 o - O 3 0 1 -
+R B 1949 1952 - D 1 0 1 -
+R B 1950 o - Ap 16 1 0 -
+R B 1951 1952 - Ap 1 0 0 -
+R B 1953 o - Mar 1 0 0 -
+R B 1963 o - D 9 0 1 -
+R B 1964 o - Mar 1 0 0 -
+R B 1965 o - Ja 31 0 1 -
+R B 1965 o - Mar 31 0 0 -
+R B 1965 o - D 1 0 1 -
+R B 1966 1968 - Mar 1 0 0 -
+R B 1966 1967 - N 1 0 1 -
+R B 1985 o - N 2 0 1 -
+R B 1986 o - Mar 15 0 0 -
+R B 1986 o - O 25 0 1 -
+R B 1987 o - F 14 0 0 -
+R B 1987 o - O 25 0 1 -
+R B 1988 o - F 7 0 0 -
+R B 1988 o - O 16 0 1 -
+R B 1989 o - Ja 29 0 0 -
+R B 1989 o - O 15 0 1 -
+R B 1990 o - F 11 0 0 -
+R B 1990 o - O 21 0 1 -
+R B 1991 o - F 17 0 0 -
+R B 1991 o - O 20 0 1 -
+R B 1992 o - F 9 0 0 -
+R B 1992 o - O 25 0 1 -
+R B 1993 o - Ja 31 0 0 -
+R B 1993 1995 - O Su>=11 0 1 -
+R B 1994 1995 - F Su>=15 0 0 -
+R B 1996 o - F 11 0 0 -
+R B 1996 o - O 6 0 1 -
+R B 1997 o - F 16 0 0 -
+R B 1997 o - O 6 0 1 -
+R B 1998 o - Mar 1 0 0 -
+R B 1998 o - O 11 0 1 -
+R B 1999 o - F 21 0 0 -
+R B 1999 o - O 3 0 1 -
+R B 2000 o - F 27 0 0 -
+R B 2000 2001 - O Su>=8 0 1 -
+R B 2001 2006 - F Su>=15 0 0 -
+R B 2002 o - N 3 0 1 -
+R B 2003 o - O 19 0 1 -
+R B 2004 o - N 2 0 1 -
+R B 2005 o - O 16 0 1 -
+R B 2006 o - N 5 0 1 -
+R B 2007 o - F 25 0 0 -
+R B 2007 o - O Su>=8 0 1 -
+R B 2008 2017 - O Su>=15 0 1 -
+R B 2008 2011 - F Su>=15 0 0 -
+R B 2012 o - F Su>=22 0 0 -
+R B 2013 2014 - F Su>=15 0 0 -
+R B 2015 o - F Su>=22 0 0 -
+R B 2016 2019 - F Su>=15 0 0 -
+R B 2018 o - N Su>=1 0 1 -
+Z America/Noronha -2:9:40 - LMT 1914
+-2 B -02/-01 1990 S 17
+-2 - -02 1999 S 30
+-2 B -02/-01 2000 O 15
+-2 - -02 2001 S 13
+-2 B -02/-01 2002 O
+-2 - -02
+Z America/Belem -3:13:56 - LMT 1914
+-3 B -03/-02 1988 S 12
+-3 - -03
+Z America/Santarem -3:38:48 - LMT 1914
+-4 B -04/-03 1988 S 12
+-4 - -04 2008 Jun 24
+-3 - -03
+Z America/Fortaleza -2:34 - LMT 1914
+-3 B -03/-02 1990 S 17
+-3 - -03 1999 S 30
+-3 B -03/-02 2000 O 22
+-3 - -03 2001 S 13
+-3 B -03/-02 2002 O
+-3 - -03
+Z America/Recife -2:19:36 - LMT 1914
+-3 B -03/-02 1990 S 17
+-3 - -03 1999 S 30
+-3 B -03/-02 2000 O 15
+-3 - -03 2001 S 13
+-3 B -03/-02 2002 O
+-3 - -03
+Z America/Araguaina -3:12:48 - LMT 1914
+-3 B -03/-02 1990 S 17
+-3 - -03 1995 S 14
+-3 B -03/-02 2003 S 24
+-3 - -03 2012 O 21
+-3 B -03/-02 2013 S
+-3 - -03
+Z America/Maceio -2:22:52 - LMT 1914
+-3 B -03/-02 1990 S 17
+-3 - -03 1995 O 13
+-3 B -03/-02 1996 S 4
+-3 - -03 1999 S 30
+-3 B -03/-02 2000 O 22
+-3 - -03 2001 S 13
+-3 B -03/-02 2002 O
+-3 - -03
+Z America/Bahia -2:34:4 - LMT 1914
+-3 B -03/-02 2003 S 24
+-3 - -03 2011 O 16
+-3 B -03/-02 2012 O 21
+-3 - -03
+Z America/Sao_Paulo -3:6:28 - LMT 1914
+-3 B -03/-02 1963 O 23
+-3 1 -02 1964
+-3 B -03/-02
+Z America/Campo_Grande -3:38:28 - LMT 1914
+-4 B -04/-03
+Z America/Cuiaba -3:44:20 - LMT 1914
+-4 B -04/-03 2003 S 24
+-4 - -04 2004 O
+-4 B -04/-03
+Z America/Porto_Velho -4:15:36 - LMT 1914
+-4 B -04/-03 1988 S 12
+-4 - -04
+Z America/Boa_Vista -4:2:40 - LMT 1914
+-4 B -04/-03 1988 S 12
+-4 - -04 1999 S 30
+-4 B -04/-03 2000 O 15
+-4 - -04
+Z America/Manaus -4:0:4 - LMT 1914
+-4 B -04/-03 1988 S 12
+-4 - -04 1993 S 28
+-4 B -04/-03 1994 S 22
+-4 - -04
+Z America/Eirunepe -4:39:28 - LMT 1914
+-5 B -05/-04 1988 S 12
+-5 - -05 1993 S 28
+-5 B -05/-04 1994 S 22
+-5 - -05 2008 Jun 24
+-4 - -04 2013 N 10
+-5 - -05
+Z America/Rio_Branco -4:31:12 - LMT 1914
+-5 B -05/-04 1988 S 12
+-5 - -05 2008 Jun 24
+-4 - -04 2013 N 10
+-5 - -05
+R x 1927 1931 - S 1 0 1 -
+R x 1928 1932 - Ap 1 0 0 -
+R x 1968 o - N 3 4u 1 -
+R x 1969 o - Mar 30 3u 0 -
+R x 1969 o - N 23 4u 1 -
+R x 1970 o - Mar 29 3u 0 -
+R x 1971 o - Mar 14 3u 0 -
+R x 1970 1972 - O Su>=9 4u 1 -
+R x 1972 1986 - Mar Su>=9 3u 0 -
+R x 1973 o - S 30 4u 1 -
+R x 1974 1987 - O Su>=9 4u 1 -
+R x 1987 o - Ap 12 3u 0 -
+R x 1988 1990 - Mar Su>=9 3u 0 -
+R x 1988 1989 - O Su>=9 4u 1 -
+R x 1990 o - S 16 4u 1 -
+R x 1991 1996 - Mar Su>=9 3u 0 -
+R x 1991 1997 - O Su>=9 4u 1 -
+R x 1997 o - Mar 30 3u 0 -
+R x 1998 o - Mar Su>=9 3u 0 -
+R x 1998 o - S 27 4u 1 -
+R x 1999 o - Ap 4 3u 0 -
+R x 1999 2010 - O Su>=9 4u 1 -
+R x 2000 2007 - Mar Su>=9 3u 0 -
+R x 2008 o - Mar 30 3u 0 -
+R x 2009 o - Mar Su>=9 3u 0 -
+R x 2010 o - Ap Su>=1 3u 0 -
+R x 2011 o - May Su>=2 3u 0 -
+R x 2011 o - Au Su>=16 4u 1 -
+R x 2012 2014 - Ap Su>=23 3u 0 -
+R x 2012 2014 - S Su>=2 4u 1 -
+R x 2016 2018 - May Su>=9 3u 0 -
+R x 2016 2018 - Au Su>=9 4u 1 -
+R x 2019 ma - Ap Su>=2 3u 0 -
+R x 2019 ma - S Su>=2 4u 1 -
+Z America/Santiago -4:42:45 - LMT 1890
+-4:42:45 - SMT 1910 Ja 10
+-5 - -05 1916 Jul
+-4:42:45 - SMT 1918 S 10
+-4 - -04 1919 Jul
+-4:42:45 - SMT 1927 S
+-5 x -05/-04 1932 S
+-4 - -04 1942 Jun
+-5 - -05 1942 Au
+-4 - -04 1946 Jul 15
+-4 1 -03 1946 S
+-4 - -04 1947 Ap
+-5 - -05 1947 May 21 23
+-4 x -04/-03
+Z America/Punta_Arenas -4:43:40 - LMT 1890
+-4:42:45 - SMT 1910 Ja 10
+-5 - -05 1916 Jul
+-4:42:45 - SMT 1918 S 10
+-4 - -04 1919 Jul
+-4:42:45 - SMT 1927 S
+-5 x -05/-04 1932 S
+-4 - -04 1942 Jun
+-5 - -05 1942 Au
+-4 - -04 1947 Ap
+-5 - -05 1947 May 21 23
+-4 x -04/-03 2016 D 4
+-3 - -03
+Z Pacific/Easter -7:17:28 - LMT 1890
+-7:17:28 - EMT 1932 S
+-7 x -07/-06 1982 Mar 14 3u
+-6 x -06/-05
+Z Antarctica/Palmer 0 - -00 1965
+-4 A -04/-03 1969 O 5
+-3 A -03/-02 1982 May
+-4 x -04/-03 2016 D 4
+-3 - -03
+R CO 1992 o - May 3 0 1 -
+R CO 1993 o - Ap 4 0 0 -
+Z America/Bogota -4:56:16 - LMT 1884 Mar 13
+-4:56:16 - BMT 1914 N 23
+-5 CO -05/-04
+R EC 1992 o - N 28 0 1 -
+R EC 1993 o - F 5 0 0 -
+Z America/Guayaquil -5:19:20 - LMT 1890
+-5:14 - QMT 1931
+-5 EC -05/-04
+Z Pacific/Galapagos -5:58:24 - LMT 1931
+-5 - -05 1986
+-6 EC -06/-05
+R FK 1937 1938 - S lastSu 0 1 -
+R FK 1938 1942 - Mar Su>=19 0 0 -
+R FK 1939 o - O 1 0 1 -
+R FK 1940 1942 - S lastSu 0 1 -
+R FK 1943 o - Ja 1 0 0 -
+R FK 1983 o - S lastSu 0 1 -
+R FK 1984 1985 - Ap lastSu 0 0 -
+R FK 1984 o - S 16 0 1 -
+R FK 1985 2000 - S Su>=9 0 1 -
+R FK 1986 2000 - Ap Su>=16 0 0 -
+R FK 2001 2010 - Ap Su>=15 2 0 -
+R FK 2001 2010 - S Su>=1 2 1 -
+Z Atlantic/Stanley -3:51:24 - LMT 1890
+-3:51:24 - SMT 1912 Mar 12
+-4 FK -04/-03 1983 May
+-3 FK -03/-02 1985 S 15
+-4 FK -04/-03 2010 S 5 2
+-3 - -03
+Z America/Cayenne -3:29:20 - LMT 1911 Jul
+-4 - -04 1967 O
+-3 - -03
+Z America/Guyana -3:52:39 - LMT 1911 Au
+-4 - -04 1915 Mar
+-3:45 - -0345 1975 Au
+-3 - -03 1992 Mar 29 1
+-4 - -04
+R y 1975 1988 - O 1 0 1 -
+R y 1975 1978 - Mar 1 0 0 -
+R y 1979 1991 - Ap 1 0 0 -
+R y 1989 o - O 22 0 1 -
+R y 1990 o - O 1 0 1 -
+R y 1991 o - O 6 0 1 -
+R y 1992 o - Mar 1 0 0 -
+R y 1992 o - O 5 0 1 -
+R y 1993 o - Mar 31 0 0 -
+R y 1993 1995 - O 1 0 1 -
+R y 1994 1995 - F lastSu 0 0 -
+R y 1996 o - Mar 1 0 0 -
+R y 1996 2001 - O Su>=1 0 1 -
+R y 1997 o - F lastSu 0 0 -
+R y 1998 2001 - Mar Su>=1 0 0 -
+R y 2002 2004 - Ap Su>=1 0 0 -
+R y 2002 2003 - S Su>=1 0 1 -
+R y 2004 2009 - O Su>=15 0 1 -
+R y 2005 2009 - Mar Su>=8 0 0 -
+R y 2010 ma - O Su>=1 0 1 -
+R y 2010 2012 - Ap Su>=8 0 0 -
+R y 2013 ma - Mar Su>=22 0 0 -
+Z America/Asuncion -3:50:40 - LMT 1890
+-3:50:40 - AMT 1931 O 10
+-4 - -04 1972 O
+-3 - -03 1974 Ap
+-4 y -04/-03
+R PE 1938 o - Ja 1 0 1 -
+R PE 1938 o - Ap 1 0 0 -
+R PE 1938 1939 - S lastSu 0 1 -
+R PE 1939 1940 - Mar Su>=24 0 0 -
+R PE 1986 1987 - Ja 1 0 1 -
+R PE 1986 1987 - Ap 1 0 0 -
+R PE 1990 o - Ja 1 0 1 -
+R PE 1990 o - Ap 1 0 0 -
+R PE 1994 o - Ja 1 0 1 -
+R PE 1994 o - Ap 1 0 0 -
+Z America/Lima -5:8:12 - LMT 1890
+-5:8:36 - LMT 1908 Jul 28
+-5 PE -05/-04
+Z Atlantic/South_Georgia -2:26:8 - LMT 1890
+-2 - -02
+Z America/Paramaribo -3:40:40 - LMT 1911
+-3:40:52 - PMT 1935
+-3:40:36 - PMT 1945 O
+-3:30 - -0330 1984 O
+-3 - -03
+R U 1923 1925 - O 1 0 0:30 -
+R U 1924 1926 - Ap 1 0 0 -
+R U 1933 1938 - O lastSu 0 0:30 -
+R U 1934 1941 - Mar lastSa 24 0 -
+R U 1939 o - O 1 0 0:30 -
+R U 1940 o - O 27 0 0:30 -
+R U 1941 o - Au 1 0 0:30 -
+R U 1942 o - D 14 0 0:30 -
+R U 1943 o - Mar 14 0 0 -
+R U 1959 o - May 24 0 0:30 -
+R U 1959 o - N 15 0 0 -
+R U 1960 o - Ja 17 0 1 -
+R U 1960 o - Mar 6 0 0 -
+R U 1965 o - Ap 4 0 1 -
+R U 1965 o - S 26 0 0 -
+R U 1968 o - May 27 0 0:30 -
+R U 1968 o - D 1 0 0 -
+R U 1970 o - Ap 25 0 1 -
+R U 1970 o - Jun 14 0 0 -
+R U 1972 o - Ap 23 0 1 -
+R U 1972 o - Jul 16 0 0 -
+R U 1974 o - Ja 13 0 1:30 -
+R U 1974 o - Mar 10 0 0:30 -
+R U 1974 o - S 1 0 0 -
+R U 1974 o - D 22 0 1 -
+R U 1975 o - Mar 30 0 0 -
+R U 1976 o - D 19 0 1 -
+R U 1977 o - Mar 6 0 0 -
+R U 1977 o - D 4 0 1 -
+R U 1978 1979 - Mar Su>=1 0 0 -
+R U 1978 o - D 17 0 1 -
+R U 1979 o - Ap 29 0 1 -
+R U 1980 o - Mar 16 0 0 -
+R U 1987 o - D 14 0 1 -
+R U 1988 o - F 28 0 0 -
+R U 1988 o - D 11 0 1 -
+R U 1989 o - Mar 5 0 0 -
+R U 1989 o - O 29 0 1 -
+R U 1990 o - F 25 0 0 -
+R U 1990 1991 - O Su>=21 0 1 -
+R U 1991 1992 - Mar Su>=1 0 0 -
+R U 1992 o - O 18 0 1 -
+R U 1993 o - F 28 0 0 -
+R U 2004 o - S 19 0 1 -
+R U 2005 o - Mar 27 2 0 -
+R U 2005 o - O 9 2 1 -
+R U 2006 2015 - Mar Su>=8 2 0 -
+R U 2006 2014 - O Su>=1 2 1 -
+Z America/Montevideo -3:44:51 - LMT 1908 Jun 10
+-3:44:51 - MMT 1920 May
+-4 - -04 1923 O
+-3:30 U -0330/-03 1942 D 14
+-3 U -03/-0230 1960
+-3 U -03/-02 1968
+-3 U -03/-0230 1970
+-3 U -03/-02 1974
+-3 U -03/-0130 1974 Mar 10
+-3 U -03/-0230 1974 D 22
+-3 U -03/-02
+Z America/Caracas -4:27:44 - LMT 1890
+-4:27:40 - CMT 1912 F 12
+-4:30 - -0430 1965
+-4 - -04 2007 D 9 3
+-4:30 - -0430 2016 May 1 2:30
+-4 - -04
+Z Etc/GMT 0 - GMT
+Z Etc/UTC 0 - UTC
+L Etc/GMT GMT
+L Etc/UTC Etc/Universal
+L Etc/UTC Etc/Zulu
+L Etc/GMT Etc/Greenwich
+L Etc/GMT Etc/GMT-0
+L Etc/GMT Etc/GMT+0
+L Etc/GMT Etc/GMT0
+Z Etc/GMT-14 14 - +14
+Z Etc/GMT-13 13 - +13
+Z Etc/GMT-12 12 - +12
+Z Etc/GMT-11 11 - +11
+Z Etc/GMT-10 10 - +10
+Z Etc/GMT-9 9 - +09
+Z Etc/GMT-8 8 - +08
+Z Etc/GMT-7 7 - +07
+Z Etc/GMT-6 6 - +06
+Z Etc/GMT-5 5 - +05
+Z Etc/GMT-4 4 - +04
+Z Etc/GMT-3 3 - +03
+Z Etc/GMT-2 2 - +02
+Z Etc/GMT-1 1 - +01
+Z Etc/GMT+1 -1 - -01
+Z Etc/GMT+2 -2 - -02
+Z Etc/GMT+3 -3 - -03
+Z Etc/GMT+4 -4 - -04
+Z Etc/GMT+5 -5 - -05
+Z Etc/GMT+6 -6 - -06
+Z Etc/GMT+7 -7 - -07
+Z Etc/GMT+8 -8 - -08
+Z Etc/GMT+9 -9 - -09
+Z Etc/GMT+10 -10 - -10
+Z Etc/GMT+11 -11 - -11
+Z Etc/GMT+12 -12 - -12
+Z Factory 0 - -00
+L Africa/Nairobi Africa/Asmera
+L Africa/Abidjan Africa/Timbuktu
+L America/Argentina/Catamarca America/Argentina/ComodRivadavia
+L America/Adak America/Atka
+L America/Argentina/Buenos_Aires America/Buenos_Aires
+L America/Argentina/Catamarca America/Catamarca
+L America/Panama America/Coral_Harbour
+L America/Argentina/Cordoba America/Cordoba
+L America/Tijuana America/Ensenada
+L America/Indiana/Indianapolis America/Fort_Wayne
+L America/Nuuk America/Godthab
+L America/Indiana/Indianapolis America/Indianapolis
+L America/Argentina/Jujuy America/Jujuy
+L America/Indiana/Knox America/Knox_IN
+L America/Kentucky/Louisville America/Louisville
+L America/Argentina/Mendoza America/Mendoza
+L America/Toronto America/Montreal
+L America/Rio_Branco America/Porto_Acre
+L America/Argentina/Cordoba America/Rosario
+L America/Tijuana America/Santa_Isabel
+L America/Denver America/Shiprock
+L America/Puerto_Rico America/Virgin
+L Pacific/Auckland Antarctica/South_Pole
+L Asia/Ashgabat Asia/Ashkhabad
+L Asia/Kolkata Asia/Calcutta
+L Asia/Shanghai Asia/Chongqing
+L Asia/Shanghai Asia/Chungking
+L Asia/Dhaka Asia/Dacca
+L Asia/Shanghai Asia/Harbin
+L Asia/Urumqi Asia/Kashgar
+L Asia/Kathmandu Asia/Katmandu
+L Asia/Macau Asia/Macao
+L Asia/Yangon Asia/Rangoon
+L Asia/Ho_Chi_Minh Asia/Saigon
+L Asia/Jerusalem Asia/Tel_Aviv
+L Asia/Thimphu Asia/Thimbu
+L Asia/Makassar Asia/Ujung_Pandang
+L Asia/Ulaanbaatar Asia/Ulan_Bator
+L Atlantic/Faroe Atlantic/Faeroe
+L Europe/Oslo Atlantic/Jan_Mayen
+L Australia/Sydney Australia/ACT
+L Australia/Sydney Australia/Canberra
+L Australia/Hobart Australia/Currie
+L Australia/Lord_Howe Australia/LHI
+L Australia/Sydney Australia/NSW
+L Australia/Darwin Australia/North
+L Australia/Brisbane Australia/Queensland
+L Australia/Adelaide Australia/South
+L Australia/Hobart Australia/Tasmania
+L Australia/Melbourne Australia/Victoria
+L Australia/Perth Australia/West
+L Australia/Broken_Hill Australia/Yancowinna
+L America/Rio_Branco Brazil/Acre
+L America/Noronha Brazil/DeNoronha
+L America/Sao_Paulo Brazil/East
+L America/Manaus Brazil/West
+L America/Halifax Canada/Atlantic
+L America/Winnipeg Canada/Central
+L America/Toronto Canada/Eastern
+L America/Edmonton Canada/Mountain
+L America/St_Johns Canada/Newfoundland
+L America/Vancouver Canada/Pacific
+L America/Regina Canada/Saskatchewan
+L America/Whitehorse Canada/Yukon
+L America/Santiago Chile/Continental
+L Pacific/Easter Chile/EasterIsland
+L America/Havana Cuba
+L Africa/Cairo Egypt
+L Europe/Dublin Eire
+L Etc/UTC Etc/UCT
+L Europe/London Europe/Belfast
+L Europe/Chisinau Europe/Tiraspol
+L Europe/London GB
+L Europe/London GB-Eire
+L Etc/GMT GMT+0
+L Etc/GMT GMT-0
+L Etc/GMT GMT0
+L Etc/GMT Greenwich
+L Asia/Hong_Kong Hongkong
+L Atlantic/Reykjavik Iceland
+L Asia/Tehran Iran
+L Asia/Jerusalem Israel
+L America/Jamaica Jamaica
+L Asia/Tokyo Japan
+L Pacific/Kwajalein Kwajalein
+L Africa/Tripoli Libya
+L America/Tijuana Mexico/BajaNorte
+L America/Mazatlan Mexico/BajaSur
+L America/Mexico_City Mexico/General
+L Pacific/Auckland NZ
+L Pacific/Chatham NZ-CHAT
+L America/Denver Navajo
+L Asia/Shanghai PRC
+L Pacific/Kanton Pacific/Enderbury
+L Pacific/Honolulu Pacific/Johnston
+L Pacific/Pohnpei Pacific/Ponape
+L Pacific/Pago_Pago Pacific/Samoa
+L Pacific/Chuuk Pacific/Truk
+L Pacific/Chuuk Pacific/Yap
+L Europe/Warsaw Poland
+L Europe/Lisbon Portugal
+L Asia/Taipei ROC
+L Asia/Seoul ROK
+L Asia/Singapore Singapore
+L Europe/Istanbul Turkey
+L Etc/UTC UCT
+L America/Anchorage US/Alaska
+L America/Adak US/Aleutian
+L America/Phoenix US/Arizona
+L America/Chicago US/Central
+L America/Indiana/Indianapolis US/East-Indiana
+L America/New_York US/Eastern
+L Pacific/Honolulu US/Hawaii
+L America/Indiana/Knox US/Indiana-Starke
+L America/Detroit US/Michigan
+L America/Denver US/Mountain
+L America/Los_Angeles US/Pacific
+L Pacific/Pago_Pago US/Samoa
+L Etc/UTC UTC
+L Etc/UTC Universal
+L Europe/Moscow W-SU
+L Etc/UTC Zulu
diff --git a/telegramer/include/pytz/zoneinfo/zone.tab b/telegramer/include/pytz/zoneinfo/zone.tab
new file mode 100644
index 0000000..086458f
--- /dev/null
+++ b/telegramer/include/pytz/zoneinfo/zone.tab
@@ -0,0 +1,454 @@
+# tzdb timezone descriptions (deprecated version)
+#
+# This file is in the public domain, so clarified as of
+# 2009-05-17 by Arthur David Olson.
+#
+# From Paul Eggert (2021-09-20):
+# This file is intended as a backward-compatibility aid for older programs.
+# New programs should use zone1970.tab. This file is like zone1970.tab (see
+# zone1970.tab's comments), but with the following additional restrictions:
+#
+# 1. This file contains only ASCII characters.
+# 2. The first data column contains exactly one country code.
+#
+# Because of (2), each row stands for an area that is the intersection
+# of a region identified by a country code and of a timezone where civil
+# clocks have agreed since 1970; this is a narrower definition than
+# that of zone1970.tab.
+#
+# Unlike zone1970.tab, a row's third column can be a Link from
+# 'backward' instead of a Zone.
+#
+# This table is intended as an aid for users, to help them select timezones
+# appropriate for their practical needs. It is not intended to take or
+# endorse any position on legal or territorial claims.
+#
+#country-
+#code coordinates TZ comments
+AD +4230+00131 Europe/Andorra
+AE +2518+05518 Asia/Dubai
+AF +3431+06912 Asia/Kabul
+AG +1703-06148 America/Antigua
+AI +1812-06304 America/Anguilla
+AL +4120+01950 Europe/Tirane
+AM +4011+04430 Asia/Yerevan
+AO -0848+01314 Africa/Luanda
+AQ -7750+16636 Antarctica/McMurdo New Zealand time - McMurdo, South Pole
+AQ -6617+11031 Antarctica/Casey Casey
+AQ -6835+07758 Antarctica/Davis Davis
+AQ -6640+14001 Antarctica/DumontDUrville Dumont-d'Urville
+AQ -6736+06253 Antarctica/Mawson Mawson
+AQ -6448-06406 Antarctica/Palmer Palmer
+AQ -6734-06808 Antarctica/Rothera Rothera
+AQ -690022+0393524 Antarctica/Syowa Syowa
+AQ -720041+0023206 Antarctica/Troll Troll
+AQ -7824+10654 Antarctica/Vostok Vostok
+AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF)
+AR -3124-06411 America/Argentina/Cordoba Argentina (most areas: CB, CC, CN, ER, FM, MN, SE, SF)
+AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN)
+AR -2411-06518 America/Argentina/Jujuy Jujuy (JY)
+AR -2649-06513 America/Argentina/Tucuman Tucuman (TM)
+AR -2828-06547 America/Argentina/Catamarca Catamarca (CT); Chubut (CH)
+AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR)
+AR -3132-06831 America/Argentina/San_Juan San Juan (SJ)
+AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ)
+AR -3319-06621 America/Argentina/San_Luis San Luis (SL)
+AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC)
+AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF)
+AS -1416-17042 Pacific/Pago_Pago
+AT +4813+01620 Europe/Vienna
+AU -3133+15905 Australia/Lord_Howe Lord Howe Island
+AU -5430+15857 Antarctica/Macquarie Macquarie Island
+AU -4253+14719 Australia/Hobart Tasmania
+AU -3749+14458 Australia/Melbourne Victoria
+AU -3352+15113 Australia/Sydney New South Wales (most areas)
+AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna)
+AU -2728+15302 Australia/Brisbane Queensland (most areas)
+AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands)
+AU -3455+13835 Australia/Adelaide South Australia
+AU -1228+13050 Australia/Darwin Northern Territory
+AU -3157+11551 Australia/Perth Western Australia (most areas)
+AU -3143+12852 Australia/Eucla Western Australia (Eucla)
+AW +1230-06958 America/Aruba
+AX +6006+01957 Europe/Mariehamn
+AZ +4023+04951 Asia/Baku
+BA +4352+01825 Europe/Sarajevo
+BB +1306-05937 America/Barbados
+BD +2343+09025 Asia/Dhaka
+BE +5050+00420 Europe/Brussels
+BF +1222-00131 Africa/Ouagadougou
+BG +4241+02319 Europe/Sofia
+BH +2623+05035 Asia/Bahrain
+BI -0323+02922 Africa/Bujumbura
+BJ +0629+00237 Africa/Porto-Novo
+BL +1753-06251 America/St_Barthelemy
+BM +3217-06446 Atlantic/Bermuda
+BN +0456+11455 Asia/Brunei
+BO -1630-06809 America/La_Paz
+BQ +120903-0681636 America/Kralendijk
+BR -0351-03225 America/Noronha Atlantic islands
+BR -0127-04829 America/Belem Para (east); Amapa
+BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB)
+BR -0803-03454 America/Recife Pernambuco
+BR -0712-04812 America/Araguaina Tocantins
+BR -0940-03543 America/Maceio Alagoas, Sergipe
+BR -1259-03831 America/Bahia Bahia
+BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS)
+BR -2027-05437 America/Campo_Grande Mato Grosso do Sul
+BR -1535-05605 America/Cuiaba Mato Grosso
+BR -0226-05452 America/Santarem Para (west)
+BR -0846-06354 America/Porto_Velho Rondonia
+BR +0249-06040 America/Boa_Vista Roraima
+BR -0308-06001 America/Manaus Amazonas (east)
+BR -0640-06952 America/Eirunepe Amazonas (west)
+BR -0958-06748 America/Rio_Branco Acre
+BS +2505-07721 America/Nassau
+BT +2728+08939 Asia/Thimphu
+BW -2439+02555 Africa/Gaborone
+BY +5354+02734 Europe/Minsk
+BZ +1730-08812 America/Belize
+CA +4734-05243 America/St_Johns Newfoundland; Labrador (southeast)
+CA +4439-06336 America/Halifax Atlantic - NS (most areas); PE
+CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton)
+CA +4606-06447 America/Moncton Atlantic - New Brunswick
+CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas)
+CA +5125-05707 America/Blanc-Sablon AST - QC (Lower North Shore)
+CA +4339-07923 America/Toronto Eastern - ON, QC (most areas)
+CA +4901-08816 America/Nipigon Eastern - ON, QC (no DST 1967-73)
+CA +4823-08915 America/Thunder_Bay Eastern - ON (Thunder Bay)
+CA +6344-06828 America/Iqaluit Eastern - NU (most east areas)
+CA +6608-06544 America/Pangnirtung Eastern - NU (Pangnirtung)
+CA +484531-0913718 America/Atikokan EST - ON (Atikokan); NU (Coral H)
+CA +4953-09709 America/Winnipeg Central - ON (west); Manitoba
+CA +4843-09434 America/Rainy_River Central - ON (Rainy R, Ft Frances)
+CA +744144-0944945 America/Resolute Central - NU (Resolute)
+CA +624900-0920459 America/Rankin_Inlet Central - NU (central)
+CA +5024-10439 America/Regina CST - SK (most areas)
+CA +5017-10750 America/Swift_Current CST - SK (midwest)
+CA +5333-11328 America/Edmonton Mountain - AB; BC (E); SK (W)
+CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west)
+CA +6227-11421 America/Yellowknife Mountain - NT (central)
+CA +682059-1334300 America/Inuvik Mountain - NT (west)
+CA +4906-11631 America/Creston MST - BC (Creston)
+CA +5946-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John)
+CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson)
+CA +6043-13503 America/Whitehorse MST - Yukon (east)
+CA +6404-13925 America/Dawson MST - Yukon (west)
+CA +4916-12307 America/Vancouver Pacific - BC (most areas)
+CC -1210+09655 Indian/Cocos
+CD -0418+01518 Africa/Kinshasa Dem. Rep. of Congo (west)
+CD -1140+02728 Africa/Lubumbashi Dem. Rep. of Congo (east)
+CF +0422+01835 Africa/Bangui
+CG -0416+01517 Africa/Brazzaville
+CH +4723+00832 Europe/Zurich
+CI +0519-00402 Africa/Abidjan
+CK -2114-15946 Pacific/Rarotonga
+CL -3327-07040 America/Santiago Chile (most areas)
+CL -5309-07055 America/Punta_Arenas Region of Magallanes
+CL -2709-10926 Pacific/Easter Easter Island
+CM +0403+00942 Africa/Douala
+CN +3114+12128 Asia/Shanghai Beijing Time
+CN +4348+08735 Asia/Urumqi Xinjiang Time
+CO +0436-07405 America/Bogota
+CR +0956-08405 America/Costa_Rica
+CU +2308-08222 America/Havana
+CV +1455-02331 Atlantic/Cape_Verde
+CW +1211-06900 America/Curacao
+CX -1025+10543 Indian/Christmas
+CY +3510+03322 Asia/Nicosia Cyprus (most areas)
+CY +3507+03357 Asia/Famagusta Northern Cyprus
+CZ +5005+01426 Europe/Prague
+DE +5230+01322 Europe/Berlin Germany (most areas)
+DE +4742+00841 Europe/Busingen Busingen
+DJ +1136+04309 Africa/Djibouti
+DK +5540+01235 Europe/Copenhagen
+DM +1518-06124 America/Dominica
+DO +1828-06954 America/Santo_Domingo
+DZ +3647+00303 Africa/Algiers
+EC -0210-07950 America/Guayaquil Ecuador (mainland)
+EC -0054-08936 Pacific/Galapagos Galapagos Islands
+EE +5925+02445 Europe/Tallinn
+EG +3003+03115 Africa/Cairo
+EH +2709-01312 Africa/El_Aaiun
+ER +1520+03853 Africa/Asmara
+ES +4024-00341 Europe/Madrid Spain (mainland)
+ES +3553-00519 Africa/Ceuta Ceuta, Melilla
+ES +2806-01524 Atlantic/Canary Canary Islands
+ET +0902+03842 Africa/Addis_Ababa
+FI +6010+02458 Europe/Helsinki
+FJ -1808+17825 Pacific/Fiji
+FK -5142-05751 Atlantic/Stanley
+FM +0725+15147 Pacific/Chuuk Chuuk/Truk, Yap
+FM +0658+15813 Pacific/Pohnpei Pohnpei/Ponape
+FM +0519+16259 Pacific/Kosrae Kosrae
+FO +6201-00646 Atlantic/Faroe
+FR +4852+00220 Europe/Paris
+GA +0023+00927 Africa/Libreville
+GB +513030-0000731 Europe/London
+GD +1203-06145 America/Grenada
+GE +4143+04449 Asia/Tbilisi
+GF +0456-05220 America/Cayenne
+GG +492717-0023210 Europe/Guernsey
+GH +0533-00013 Africa/Accra
+GI +3608-00521 Europe/Gibraltar
+GL +6411-05144 America/Nuuk Greenland (most areas)
+GL +7646-01840 America/Danmarkshavn National Park (east coast)
+GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit
+GL +7634-06847 America/Thule Thule/Pituffik
+GM +1328-01639 Africa/Banjul
+GN +0931-01343 Africa/Conakry
+GP +1614-06132 America/Guadeloupe
+GQ +0345+00847 Africa/Malabo
+GR +3758+02343 Europe/Athens
+GS -5416-03632 Atlantic/South_Georgia
+GT +1438-09031 America/Guatemala
+GU +1328+14445 Pacific/Guam
+GW +1151-01535 Africa/Bissau
+GY +0648-05810 America/Guyana
+HK +2217+11409 Asia/Hong_Kong
+HN +1406-08713 America/Tegucigalpa
+HR +4548+01558 Europe/Zagreb
+HT +1832-07220 America/Port-au-Prince
+HU +4730+01905 Europe/Budapest
+ID -0610+10648 Asia/Jakarta Java, Sumatra
+ID -0002+10920 Asia/Pontianak Borneo (west, central)
+ID -0507+11924 Asia/Makassar Borneo (east, south); Sulawesi/Celebes, Bali, Nusa Tengarra; Timor (west)
+ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya); Malukus/Moluccas
+IE +5320-00615 Europe/Dublin
+IL +314650+0351326 Asia/Jerusalem
+IM +5409-00428 Europe/Isle_of_Man
+IN +2232+08822 Asia/Kolkata
+IO -0720+07225 Indian/Chagos
+IQ +3321+04425 Asia/Baghdad
+IR +3540+05126 Asia/Tehran
+IS +6409-02151 Atlantic/Reykjavik
+IT +4154+01229 Europe/Rome
+JE +491101-0020624 Europe/Jersey
+JM +175805-0764736 America/Jamaica
+JO +3157+03556 Asia/Amman
+JP +353916+1394441 Asia/Tokyo
+KE -0117+03649 Africa/Nairobi
+KG +4254+07436 Asia/Bishkek
+KH +1133+10455 Asia/Phnom_Penh
+KI +0125+17300 Pacific/Tarawa Gilbert Islands
+KI -0247-17143 Pacific/Kanton Phoenix Islands
+KI +0152-15720 Pacific/Kiritimati Line Islands
+KM -1141+04316 Indian/Comoro
+KN +1718-06243 America/St_Kitts
+KP +3901+12545 Asia/Pyongyang
+KR +3733+12658 Asia/Seoul
+KW +2920+04759 Asia/Kuwait
+KY +1918-08123 America/Cayman
+KZ +4315+07657 Asia/Almaty Kazakhstan (most areas)
+KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda
+KZ +5312+06337 Asia/Qostanay Qostanay/Kostanay/Kustanay
+KZ +5017+05710 Asia/Aqtobe Aqtobe/Aktobe
+KZ +4431+05016 Asia/Aqtau Mangghystau/Mankistau
+KZ +4707+05156 Asia/Atyrau Atyrau/Atirau/Gur'yev
+KZ +5113+05121 Asia/Oral West Kazakhstan
+LA +1758+10236 Asia/Vientiane
+LB +3353+03530 Asia/Beirut
+LC +1401-06100 America/St_Lucia
+LI +4709+00931 Europe/Vaduz
+LK +0656+07951 Asia/Colombo
+LR +0618-01047 Africa/Monrovia
+LS -2928+02730 Africa/Maseru
+LT +5441+02519 Europe/Vilnius
+LU +4936+00609 Europe/Luxembourg
+LV +5657+02406 Europe/Riga
+LY +3254+01311 Africa/Tripoli
+MA +3339-00735 Africa/Casablanca
+MC +4342+00723 Europe/Monaco
+MD +4700+02850 Europe/Chisinau
+ME +4226+01916 Europe/Podgorica
+MF +1804-06305 America/Marigot
+MG -1855+04731 Indian/Antananarivo
+MH +0709+17112 Pacific/Majuro Marshall Islands (most areas)
+MH +0905+16720 Pacific/Kwajalein Kwajalein
+MK +4159+02126 Europe/Skopje
+ML +1239-00800 Africa/Bamako
+MM +1647+09610 Asia/Yangon
+MN +4755+10653 Asia/Ulaanbaatar Mongolia (most areas)
+MN +4801+09139 Asia/Hovd Bayan-Olgiy, Govi-Altai, Hovd, Uvs, Zavkhan
+MN +4804+11430 Asia/Choibalsan Dornod, Sukhbaatar
+MO +221150+1133230 Asia/Macau
+MP +1512+14545 Pacific/Saipan
+MQ +1436-06105 America/Martinique
+MR +1806-01557 Africa/Nouakchott
+MS +1643-06213 America/Montserrat
+MT +3554+01431 Europe/Malta
+MU -2010+05730 Indian/Mauritius
+MV +0410+07330 Indian/Maldives
+MW -1547+03500 Africa/Blantyre
+MX +1924-09909 America/Mexico_City Central Time
+MX +2105-08646 America/Cancun Eastern Standard Time - Quintana Roo
+MX +2058-08937 America/Merida Central Time - Campeche, Yucatan
+MX +2540-10019 America/Monterrey Central Time - Durango; Coahuila, Nuevo Leon, Tamaulipas (most areas)
+MX +2550-09730 America/Matamoros Central Time US - Coahuila, Nuevo Leon, Tamaulipas (US border)
+MX +2313-10625 America/Mazatlan Mountain Time - Baja California Sur, Nayarit, Sinaloa
+MX +2838-10605 America/Chihuahua Mountain Time - Chihuahua (most areas)
+MX +2934-10425 America/Ojinaga Mountain Time US - Chihuahua (US border)
+MX +2904-11058 America/Hermosillo Mountain Standard Time - Sonora
+MX +3232-11701 America/Tijuana Pacific Time US - Baja California
+MX +2048-10515 America/Bahia_Banderas Central Time - Bahia de Banderas
+MY +0310+10142 Asia/Kuala_Lumpur Malaysia (peninsula)
+MY +0133+11020 Asia/Kuching Sabah, Sarawak
+MZ -2558+03235 Africa/Maputo
+NA -2234+01706 Africa/Windhoek
+NC -2216+16627 Pacific/Noumea
+NE +1331+00207 Africa/Niamey
+NF -2903+16758 Pacific/Norfolk
+NG +0627+00324 Africa/Lagos
+NI +1209-08617 America/Managua
+NL +5222+00454 Europe/Amsterdam
+NO +5955+01045 Europe/Oslo
+NP +2743+08519 Asia/Kathmandu
+NR -0031+16655 Pacific/Nauru
+NU -1901-16955 Pacific/Niue
+NZ -3652+17446 Pacific/Auckland New Zealand (most areas)
+NZ -4357-17633 Pacific/Chatham Chatham Islands
+OM +2336+05835 Asia/Muscat
+PA +0858-07932 America/Panama
+PE -1203-07703 America/Lima
+PF -1732-14934 Pacific/Tahiti Society Islands
+PF -0900-13930 Pacific/Marquesas Marquesas Islands
+PF -2308-13457 Pacific/Gambier Gambier Islands
+PG -0930+14710 Pacific/Port_Moresby Papua New Guinea (most areas)
+PG -0613+15534 Pacific/Bougainville Bougainville
+PH +1435+12100 Asia/Manila
+PK +2452+06703 Asia/Karachi
+PL +5215+02100 Europe/Warsaw
+PM +4703-05620 America/Miquelon
+PN -2504-13005 Pacific/Pitcairn
+PR +182806-0660622 America/Puerto_Rico
+PS +3130+03428 Asia/Gaza Gaza Strip
+PS +313200+0350542 Asia/Hebron West Bank
+PT +3843-00908 Europe/Lisbon Portugal (mainland)
+PT +3238-01654 Atlantic/Madeira Madeira Islands
+PT +3744-02540 Atlantic/Azores Azores
+PW +0720+13429 Pacific/Palau
+PY -2516-05740 America/Asuncion
+QA +2517+05132 Asia/Qatar
+RE -2052+05528 Indian/Reunion
+RO +4426+02606 Europe/Bucharest
+RS +4450+02030 Europe/Belgrade
+RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad
+RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area
+# The obsolescent zone.tab format cannot represent Europe/Simferopol well.
+# Put it in RU section and list as UA. See "territorial claims" above.
+# Programs should use zone1970.tab instead; see above.
+UA +4457+03406 Europe/Simferopol Crimea
+RU +5836+04939 Europe/Kirov MSK+00 - Kirov
+RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd
+RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan
+RU +5134+04602 Europe/Saratov MSK+01 - Saratov
+RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk
+RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia
+RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals
+RU +5500+07324 Asia/Omsk MSK+03 - Omsk
+RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk
+RU +5322+08345 Asia/Barnaul MSK+04 - Altai
+RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk
+RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo
+RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area
+RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia
+RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky
+RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River
+RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky
+RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River
+RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky
+RU +5934+15048 Asia/Magadan MSK+08 - Magadan
+RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island
+RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E); North Kuril Is
+RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka
+RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea
+RW -0157+03004 Africa/Kigali
+SA +2438+04643 Asia/Riyadh
+SB -0932+16012 Pacific/Guadalcanal
+SC -0440+05528 Indian/Mahe
+SD +1536+03232 Africa/Khartoum
+SE +5920+01803 Europe/Stockholm
+SG +0117+10351 Asia/Singapore
+SH -1555-00542 Atlantic/St_Helena
+SI +4603+01431 Europe/Ljubljana
+SJ +7800+01600 Arctic/Longyearbyen
+SK +4809+01707 Europe/Bratislava
+SL +0830-01315 Africa/Freetown
+SM +4355+01228 Europe/San_Marino
+SN +1440-01726 Africa/Dakar
+SO +0204+04522 Africa/Mogadishu
+SR +0550-05510 America/Paramaribo
+SS +0451+03137 Africa/Juba
+ST +0020+00644 Africa/Sao_Tome
+SV +1342-08912 America/El_Salvador
+SX +180305-0630250 America/Lower_Princes
+SY +3330+03618 Asia/Damascus
+SZ -2618+03106 Africa/Mbabane
+TC +2128-07108 America/Grand_Turk
+TD +1207+01503 Africa/Ndjamena
+TF -492110+0701303 Indian/Kerguelen
+TG +0608+00113 Africa/Lome
+TH +1345+10031 Asia/Bangkok
+TJ +3835+06848 Asia/Dushanbe
+TK -0922-17114 Pacific/Fakaofo
+TL -0833+12535 Asia/Dili
+TM +3757+05823 Asia/Ashgabat
+TN +3648+01011 Africa/Tunis
+TO -210800-1751200 Pacific/Tongatapu
+TR +4101+02858 Europe/Istanbul
+TT +1039-06131 America/Port_of_Spain
+TV -0831+17913 Pacific/Funafuti
+TW +2503+12130 Asia/Taipei
+TZ -0648+03917 Africa/Dar_es_Salaam
+UA +5026+03031 Europe/Kiev Ukraine (most areas)
+UA +4837+02218 Europe/Uzhgorod Transcarpathia
+UA +4750+03510 Europe/Zaporozhye Zaporozhye and east Lugansk
+UG +0019+03225 Africa/Kampala
+UM +2813-17722 Pacific/Midway Midway Islands
+UM +1917+16637 Pacific/Wake Wake Island
+US +404251-0740023 America/New_York Eastern (most areas)
+US +421953-0830245 America/Detroit Eastern - MI (most areas)
+US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area)
+US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne)
+US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas)
+US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn)
+US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski)
+US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford)
+US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike)
+US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland)
+US +415100-0873900 America/Chicago Central (most areas)
+US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry)
+US +411745-0863730 America/Indiana/Knox Central - IN (Starke)
+US +450628-0873651 America/Menominee Central - MI (Wisconsin border)
+US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver)
+US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural)
+US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer)
+US +394421-1045903 America/Denver Mountain (most areas)
+US +433649-1161209 America/Boise Mountain - ID (south); OR (east)
+US +332654-1120424 America/Phoenix MST - Arizona (except Navajo)
+US +340308-1181434 America/Los_Angeles Pacific
+US +611305-1495401 America/Anchorage Alaska (most areas)
+US +581807-1342511 America/Juneau Alaska - Juneau area
+US +571035-1351807 America/Sitka Alaska - Sitka area
+US +550737-1313435 America/Metlakatla Alaska - Annette Island
+US +593249-1394338 America/Yakutat Alaska - Yakutat
+US +643004-1652423 America/Nome Alaska (west)
+US +515248-1763929 America/Adak Aleutian Islands
+US +211825-1575130 Pacific/Honolulu Hawaii
+UY -345433-0561245 America/Montevideo
+UZ +3940+06648 Asia/Samarkand Uzbekistan (west)
+UZ +4120+06918 Asia/Tashkent Uzbekistan (east)
+VA +415408+0122711 Europe/Vatican
+VC +1309-06114 America/St_Vincent
+VE +1030-06656 America/Caracas
+VG +1827-06437 America/Tortola
+VI +1821-06456 America/St_Thomas
+VN +1045+10640 Asia/Ho_Chi_Minh
+VU -1740+16825 Pacific/Efate
+WF -1318-17610 Pacific/Wallis
+WS -1350-17144 Pacific/Apia
+YE +1245+04512 Asia/Aden
+YT -1247+04514 Indian/Mayotte
+ZA -2615+02800 Africa/Johannesburg
+ZM -1525+02817 Africa/Lusaka
+ZW -1750+03103 Africa/Harare
diff --git a/telegramer/include/pytz/zoneinfo/zone1970.tab b/telegramer/include/pytz/zoneinfo/zone1970.tab
new file mode 100644
index 0000000..c614be8
--- /dev/null
+++ b/telegramer/include/pytz/zoneinfo/zone1970.tab
@@ -0,0 +1,374 @@
+# tzdb timezone descriptions
+#
+# This file is in the public domain.
+#
+# From Paul Eggert (2018-06-27):
+# This file contains a table where each row stands for a timezone where
+# civil timestamps have agreed since 1970. Columns are separated by
+# a single tab. Lines beginning with '#' are comments. All text uses
+# UTF-8 encoding. The columns of the table are as follows:
+#
+# 1. The countries that overlap the timezone, as a comma-separated list
+# of ISO 3166 2-character country codes. See the file 'iso3166.tab'.
+# 2. Latitude and longitude of the timezone's principal location
+# in ISO 6709 sign-degrees-minutes-seconds format,
+# either ±DDMM±DDDMM or ±DDMMSS±DDDMMSS,
+# first latitude (+ is north), then longitude (+ is east).
+# 3. Timezone name used in value of TZ environment variable.
+# Please see the theory.html file for how these names are chosen.
+# If multiple timezones overlap a country, each has a row in the
+# table, with each column 1 containing the country code.
+# 4. Comments; present if and only if a country has multiple timezones.
+#
+# If a timezone covers multiple countries, the most-populous city is used,
+# and that country is listed first in column 1; any other countries
+# are listed alphabetically by country code. The table is sorted
+# first by country code, then (if possible) by an order within the
+# country that (1) makes some geographical sense, and (2) puts the
+# most populous timezones first, where that does not contradict (1).
+#
+# This table is intended as an aid for users, to help them select timezones
+# appropriate for their practical needs. It is not intended to take or
+# endorse any position on legal or territorial claims.
+#
+#country-
+#codes coordinates TZ comments
+AD +4230+00131 Europe/Andorra
+AE,OM +2518+05518 Asia/Dubai
+AF +3431+06912 Asia/Kabul
+AL +4120+01950 Europe/Tirane
+AM +4011+04430 Asia/Yerevan
+AQ -6617+11031 Antarctica/Casey Casey
+AQ -6835+07758 Antarctica/Davis Davis
+AQ -6736+06253 Antarctica/Mawson Mawson
+AQ -6448-06406 Antarctica/Palmer Palmer
+AQ -6734-06808 Antarctica/Rothera Rothera
+AQ -720041+0023206 Antarctica/Troll Troll
+AQ -7824+10654 Antarctica/Vostok Vostok
+AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF)
+AR -3124-06411 America/Argentina/Cordoba Argentina (most areas: CB, CC, CN, ER, FM, MN, SE, SF)
+AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN)
+AR -2411-06518 America/Argentina/Jujuy Jujuy (JY)
+AR -2649-06513 America/Argentina/Tucuman Tucumán (TM)
+AR -2828-06547 America/Argentina/Catamarca Catamarca (CT); Chubut (CH)
+AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR)
+AR -3132-06831 America/Argentina/San_Juan San Juan (SJ)
+AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ)
+AR -3319-06621 America/Argentina/San_Luis San Luis (SL)
+AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC)
+AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF)
+AS,UM -1416-17042 Pacific/Pago_Pago Samoa, Midway
+AT +4813+01620 Europe/Vienna
+AU -3133+15905 Australia/Lord_Howe Lord Howe Island
+AU -5430+15857 Antarctica/Macquarie Macquarie Island
+AU -4253+14719 Australia/Hobart Tasmania
+AU -3749+14458 Australia/Melbourne Victoria
+AU -3352+15113 Australia/Sydney New South Wales (most areas)
+AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna)
+AU -2728+15302 Australia/Brisbane Queensland (most areas)
+AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands)
+AU -3455+13835 Australia/Adelaide South Australia
+AU -1228+13050 Australia/Darwin Northern Territory
+AU -3157+11551 Australia/Perth Western Australia (most areas)
+AU -3143+12852 Australia/Eucla Western Australia (Eucla)
+AZ +4023+04951 Asia/Baku
+BB +1306-05937 America/Barbados
+BD +2343+09025 Asia/Dhaka
+BE +5050+00420 Europe/Brussels
+BG +4241+02319 Europe/Sofia
+BM +3217-06446 Atlantic/Bermuda
+BN +0456+11455 Asia/Brunei
+BO -1630-06809 America/La_Paz
+BR -0351-03225 America/Noronha Atlantic islands
+BR -0127-04829 America/Belem Pará (east); Amapá
+BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB)
+BR -0803-03454 America/Recife Pernambuco
+BR -0712-04812 America/Araguaina Tocantins
+BR -0940-03543 America/Maceio Alagoas, Sergipe
+BR -1259-03831 America/Bahia Bahia
+BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS)
+BR -2027-05437 America/Campo_Grande Mato Grosso do Sul
+BR -1535-05605 America/Cuiaba Mato Grosso
+BR -0226-05452 America/Santarem Pará (west)
+BR -0846-06354 America/Porto_Velho Rondônia
+BR +0249-06040 America/Boa_Vista Roraima
+BR -0308-06001 America/Manaus Amazonas (east)
+BR -0640-06952 America/Eirunepe Amazonas (west)
+BR -0958-06748 America/Rio_Branco Acre
+BT +2728+08939 Asia/Thimphu
+BY +5354+02734 Europe/Minsk
+BZ +1730-08812 America/Belize
+CA +4734-05243 America/St_Johns Newfoundland; Labrador (southeast)
+CA +4439-06336 America/Halifax Atlantic - NS (most areas); PE
+CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton)
+CA +4606-06447 America/Moncton Atlantic - New Brunswick
+CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas)
+CA,BS +4339-07923 America/Toronto Eastern - ON, QC (most areas), Bahamas
+CA +4901-08816 America/Nipigon Eastern - ON, QC (no DST 1967-73)
+CA +4823-08915 America/Thunder_Bay Eastern - ON (Thunder Bay)
+CA +6344-06828 America/Iqaluit Eastern - NU (most east areas)
+CA +6608-06544 America/Pangnirtung Eastern - NU (Pangnirtung)
+CA +4953-09709 America/Winnipeg Central - ON (west); Manitoba
+CA +4843-09434 America/Rainy_River Central - ON (Rainy R, Ft Frances)
+CA +744144-0944945 America/Resolute Central - NU (Resolute)
+CA +624900-0920459 America/Rankin_Inlet Central - NU (central)
+CA +5024-10439 America/Regina CST - SK (most areas)
+CA +5017-10750 America/Swift_Current CST - SK (midwest)
+CA +5333-11328 America/Edmonton Mountain - AB; BC (E); SK (W)
+CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west)
+CA +6227-11421 America/Yellowknife Mountain - NT (central)
+CA +682059-1334300 America/Inuvik Mountain - NT (west)
+CA +5946-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John)
+CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson)
+CA +6043-13503 America/Whitehorse MST - Yukon (east)
+CA +6404-13925 America/Dawson MST - Yukon (west)
+CA +4916-12307 America/Vancouver Pacific - BC (most areas)
+CC -1210+09655 Indian/Cocos
+CH,DE,LI +4723+00832 Europe/Zurich Swiss time
+CI,BF,GH,GM,GN,ML,MR,SH,SL,SN,TG +0519-00402 Africa/Abidjan
+CK -2114-15946 Pacific/Rarotonga
+CL -3327-07040 America/Santiago Chile (most areas)
+CL -5309-07055 America/Punta_Arenas Region of Magallanes
+CL -2709-10926 Pacific/Easter Easter Island
+CN +3114+12128 Asia/Shanghai Beijing Time
+CN +4348+08735 Asia/Urumqi Xinjiang Time
+CO +0436-07405 America/Bogota
+CR +0956-08405 America/Costa_Rica
+CU +2308-08222 America/Havana
+CV +1455-02331 Atlantic/Cape_Verde
+CX -1025+10543 Indian/Christmas
+CY +3510+03322 Asia/Nicosia Cyprus (most areas)
+CY +3507+03357 Asia/Famagusta Northern Cyprus
+CZ,SK +5005+01426 Europe/Prague
+DE +5230+01322 Europe/Berlin Germany (most areas)
+DK +5540+01235 Europe/Copenhagen
+DO +1828-06954 America/Santo_Domingo
+DZ +3647+00303 Africa/Algiers
+EC -0210-07950 America/Guayaquil Ecuador (mainland)
+EC -0054-08936 Pacific/Galapagos Galápagos Islands
+EE +5925+02445 Europe/Tallinn
+EG +3003+03115 Africa/Cairo
+EH +2709-01312 Africa/El_Aaiun
+ES +4024-00341 Europe/Madrid Spain (mainland)
+ES +3553-00519 Africa/Ceuta Ceuta, Melilla
+ES +2806-01524 Atlantic/Canary Canary Islands
+FI,AX +6010+02458 Europe/Helsinki
+FJ -1808+17825 Pacific/Fiji
+FK -5142-05751 Atlantic/Stanley
+FM +0725+15147 Pacific/Chuuk Chuuk/Truk, Yap
+FM +0658+15813 Pacific/Pohnpei Pohnpei/Ponape
+FM +0519+16259 Pacific/Kosrae Kosrae
+FO +6201-00646 Atlantic/Faroe
+FR +4852+00220 Europe/Paris
+GB,GG,IM,JE +513030-0000731 Europe/London
+GE +4143+04449 Asia/Tbilisi
+GF +0456-05220 America/Cayenne
+GI +3608-00521 Europe/Gibraltar
+GL +6411-05144 America/Nuuk Greenland (most areas)
+GL +7646-01840 America/Danmarkshavn National Park (east coast)
+GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit
+GL +7634-06847 America/Thule Thule/Pituffik
+GR +3758+02343 Europe/Athens
+GS -5416-03632 Atlantic/South_Georgia
+GT +1438-09031 America/Guatemala
+GU,MP +1328+14445 Pacific/Guam
+GW +1151-01535 Africa/Bissau
+GY +0648-05810 America/Guyana
+HK +2217+11409 Asia/Hong_Kong
+HN +1406-08713 America/Tegucigalpa
+HT +1832-07220 America/Port-au-Prince
+HU +4730+01905 Europe/Budapest
+ID -0610+10648 Asia/Jakarta Java, Sumatra
+ID -0002+10920 Asia/Pontianak Borneo (west, central)
+ID -0507+11924 Asia/Makassar Borneo (east, south); Sulawesi/Celebes, Bali, Nusa Tengarra; Timor (west)
+ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya); Malukus/Moluccas
+IE +5320-00615 Europe/Dublin
+IL +314650+0351326 Asia/Jerusalem
+IN +2232+08822 Asia/Kolkata
+IO -0720+07225 Indian/Chagos
+IQ +3321+04425 Asia/Baghdad
+IR +3540+05126 Asia/Tehran
+IS +6409-02151 Atlantic/Reykjavik
+IT,SM,VA +4154+01229 Europe/Rome
+JM +175805-0764736 America/Jamaica
+JO +3157+03556 Asia/Amman
+JP +353916+1394441 Asia/Tokyo
+KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT -0117+03649 Africa/Nairobi
+KG +4254+07436 Asia/Bishkek
+KI +0125+17300 Pacific/Tarawa Gilbert Islands
+KI -0247-17143 Pacific/Kanton Phoenix Islands
+KI +0152-15720 Pacific/Kiritimati Line Islands
+KP +3901+12545 Asia/Pyongyang
+KR +3733+12658 Asia/Seoul
+KZ +4315+07657 Asia/Almaty Kazakhstan (most areas)
+KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda
+KZ +5312+06337 Asia/Qostanay Qostanay/Kostanay/Kustanay
+KZ +5017+05710 Asia/Aqtobe Aqtöbe/Aktobe
+KZ +4431+05016 Asia/Aqtau Mangghystaū/Mankistau
+KZ +4707+05156 Asia/Atyrau Atyraū/Atirau/Gur'yev
+KZ +5113+05121 Asia/Oral West Kazakhstan
+LB +3353+03530 Asia/Beirut
+LK +0656+07951 Asia/Colombo
+LR +0618-01047 Africa/Monrovia
+LT +5441+02519 Europe/Vilnius
+LU +4936+00609 Europe/Luxembourg
+LV +5657+02406 Europe/Riga
+LY +3254+01311 Africa/Tripoli
+MA +3339-00735 Africa/Casablanca
+MC +4342+00723 Europe/Monaco
+MD +4700+02850 Europe/Chisinau
+MH +0709+17112 Pacific/Majuro Marshall Islands (most areas)
+MH +0905+16720 Pacific/Kwajalein Kwajalein
+MM +1647+09610 Asia/Yangon
+MN +4755+10653 Asia/Ulaanbaatar Mongolia (most areas)
+MN +4801+09139 Asia/Hovd Bayan-Ölgii, Govi-Altai, Hovd, Uvs, Zavkhan
+MN +4804+11430 Asia/Choibalsan Dornod, Sükhbaatar
+MO +221150+1133230 Asia/Macau
+MQ +1436-06105 America/Martinique
+MT +3554+01431 Europe/Malta
+MU -2010+05730 Indian/Mauritius
+MV +0410+07330 Indian/Maldives
+MX +1924-09909 America/Mexico_City Central Time
+MX +2105-08646 America/Cancun Eastern Standard Time - Quintana Roo
+MX +2058-08937 America/Merida Central Time - Campeche, Yucatán
+MX +2540-10019 America/Monterrey Central Time - Durango; Coahuila, Nuevo León, Tamaulipas (most areas)
+MX +2550-09730 America/Matamoros Central Time US - Coahuila, Nuevo León, Tamaulipas (US border)
+MX +2313-10625 America/Mazatlan Mountain Time - Baja California Sur, Nayarit, Sinaloa
+MX +2838-10605 America/Chihuahua Mountain Time - Chihuahua (most areas)
+MX +2934-10425 America/Ojinaga Mountain Time US - Chihuahua (US border)
+MX +2904-11058 America/Hermosillo Mountain Standard Time - Sonora
+MX +3232-11701 America/Tijuana Pacific Time US - Baja California
+MX +2048-10515 America/Bahia_Banderas Central Time - Bahía de Banderas
+MY +0310+10142 Asia/Kuala_Lumpur Malaysia (peninsula)
+MY +0133+11020 Asia/Kuching Sabah, Sarawak
+MZ,BI,BW,CD,MW,RW,ZM,ZW -2558+03235 Africa/Maputo Central Africa Time
+NA -2234+01706 Africa/Windhoek
+NC -2216+16627 Pacific/Noumea
+NF -2903+16758 Pacific/Norfolk
+NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE +0627+00324 Africa/Lagos West Africa Time
+NI +1209-08617 America/Managua
+NL +5222+00454 Europe/Amsterdam
+NO,SJ +5955+01045 Europe/Oslo
+NP +2743+08519 Asia/Kathmandu
+NR -0031+16655 Pacific/Nauru
+NU -1901-16955 Pacific/Niue
+NZ,AQ -3652+17446 Pacific/Auckland New Zealand time
+NZ -4357-17633 Pacific/Chatham Chatham Islands
+PA,CA,KY +0858-07932 America/Panama EST - Panama, Cayman, ON (Atikokan), NU (Coral H)
+PE -1203-07703 America/Lima
+PF -1732-14934 Pacific/Tahiti Society Islands
+PF -0900-13930 Pacific/Marquesas Marquesas Islands
+PF -2308-13457 Pacific/Gambier Gambier Islands
+PG,AQ -0930+14710 Pacific/Port_Moresby Papua New Guinea (most areas), Dumont d'Urville
+PG -0613+15534 Pacific/Bougainville Bougainville
+PH +1435+12100 Asia/Manila
+PK +2452+06703 Asia/Karachi
+PL +5215+02100 Europe/Warsaw
+PM +4703-05620 America/Miquelon
+PN -2504-13005 Pacific/Pitcairn
+PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI +182806-0660622 America/Puerto_Rico AST
+PS +3130+03428 Asia/Gaza Gaza Strip
+PS +313200+0350542 Asia/Hebron West Bank
+PT +3843-00908 Europe/Lisbon Portugal (mainland)
+PT +3238-01654 Atlantic/Madeira Madeira Islands
+PT +3744-02540 Atlantic/Azores Azores
+PW +0720+13429 Pacific/Palau
+PY -2516-05740 America/Asuncion
+QA,BH +2517+05132 Asia/Qatar
+RE,TF -2052+05528 Indian/Reunion Réunion, Crozet, Scattered Islands
+RO +4426+02606 Europe/Bucharest
+RS,BA,HR,ME,MK,SI +4450+02030 Europe/Belgrade
+RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad
+RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area
+# Mention RU and UA alphabetically. See "territorial claims" above.
+RU,UA +4457+03406 Europe/Simferopol Crimea
+RU +5836+04939 Europe/Kirov MSK+00 - Kirov
+RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd
+RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan
+RU +5134+04602 Europe/Saratov MSK+01 - Saratov
+RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk
+RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia
+RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals
+RU +5500+07324 Asia/Omsk MSK+03 - Omsk
+RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk
+RU +5322+08345 Asia/Barnaul MSK+04 - Altai
+RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk
+RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo
+RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area
+RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia
+RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky
+RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River
+RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky
+RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River
+RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky
+RU +5934+15048 Asia/Magadan MSK+08 - Magadan
+RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island
+RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E); North Kuril Is
+RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka
+RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea
+SA,AQ,KW,YE +2438+04643 Asia/Riyadh Arabia, Syowa
+SB -0932+16012 Pacific/Guadalcanal
+SC -0440+05528 Indian/Mahe
+SD +1536+03232 Africa/Khartoum
+SE +5920+01803 Europe/Stockholm
+SG,MY +0117+10351 Asia/Singapore Singapore, peninsular Malaysia
+SR +0550-05510 America/Paramaribo
+SS +0451+03137 Africa/Juba
+ST +0020+00644 Africa/Sao_Tome
+SV +1342-08912 America/El_Salvador
+SY +3330+03618 Asia/Damascus
+TC +2128-07108 America/Grand_Turk
+TD +1207+01503 Africa/Ndjamena
+TF -492110+0701303 Indian/Kerguelen Kerguelen, St Paul Island, Amsterdam Island
+TH,KH,LA,VN +1345+10031 Asia/Bangkok Indochina (most areas)
+TJ +3835+06848 Asia/Dushanbe
+TK -0922-17114 Pacific/Fakaofo
+TL -0833+12535 Asia/Dili
+TM +3757+05823 Asia/Ashgabat
+TN +3648+01011 Africa/Tunis
+TO -210800-1751200 Pacific/Tongatapu
+TR +4101+02858 Europe/Istanbul
+TV -0831+17913 Pacific/Funafuti
+TW +2503+12130 Asia/Taipei
+UA +5026+03031 Europe/Kiev Ukraine (most areas)
+UA +4837+02218 Europe/Uzhgorod Transcarpathia
+UA +4750+03510 Europe/Zaporozhye Zaporozhye and east Lugansk
+UM +1917+16637 Pacific/Wake Wake Island
+US +404251-0740023 America/New_York Eastern (most areas)
+US +421953-0830245 America/Detroit Eastern - MI (most areas)
+US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area)
+US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne)
+US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas)
+US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn)
+US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski)
+US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford)
+US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike)
+US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland)
+US +415100-0873900 America/Chicago Central (most areas)
+US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry)
+US +411745-0863730 America/Indiana/Knox Central - IN (Starke)
+US +450628-0873651 America/Menominee Central - MI (Wisconsin border)
+US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver)
+US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural)
+US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer)
+US +394421-1045903 America/Denver Mountain (most areas)
+US +433649-1161209 America/Boise Mountain - ID (south); OR (east)
+US,CA +332654-1120424 America/Phoenix MST - Arizona (except Navajo), Creston BC
+US +340308-1181434 America/Los_Angeles Pacific
+US +611305-1495401 America/Anchorage Alaska (most areas)
+US +581807-1342511 America/Juneau Alaska - Juneau area
+US +571035-1351807 America/Sitka Alaska - Sitka area
+US +550737-1313435 America/Metlakatla Alaska - Annette Island
+US +593249-1394338 America/Yakutat Alaska - Yakutat
+US +643004-1652423 America/Nome Alaska (west)
+US +515248-1763929 America/Adak Aleutian Islands
+US,UM +211825-1575130 Pacific/Honolulu Hawaii
+UY -345433-0561245 America/Montevideo
+UZ +3940+06648 Asia/Samarkand Uzbekistan (west)
+UZ +4120+06918 Asia/Tashkent Uzbekistan (east)
+VE +1030-06656 America/Caracas
+VN +1045+10640 Asia/Ho_Chi_Minh Vietnam (south)
+VU -1740+16825 Pacific/Efate
+WF -1318-17610 Pacific/Wallis
+WS -1350-17144 Pacific/Apia
+ZA,LS,SZ -2615+02800 Africa/Johannesburg
diff --git a/telegramer/include/pytz_deprecation_shim/__init__.py b/telegramer/include/pytz_deprecation_shim/__init__.py
new file mode 100644
index 0000000..8b45162
--- /dev/null
+++ b/telegramer/include/pytz_deprecation_shim/__init__.py
@@ -0,0 +1,34 @@
+__all__ = [
+ "AmbiguousTimeError",
+ "NonExistentTimeError",
+ "InvalidTimeError",
+ "UnknownTimeZoneError",
+ "PytzUsageWarning",
+ "FixedOffset",
+ "UTC",
+ "utc",
+ "build_tzinfo",
+ "timezone",
+ "fixed_offset_timezone",
+ "wrap_zone",
+]
+
+from . import helpers
+from ._exceptions import (
+ AmbiguousTimeError,
+ InvalidTimeError,
+ NonExistentTimeError,
+ PytzUsageWarning,
+ UnknownTimeZoneError,
+)
+from ._impl import (
+ UTC,
+ build_tzinfo,
+ fixed_offset_timezone,
+ timezone,
+ wrap_zone,
+)
+
+# Compatibility aliases
+utc = UTC
+FixedOffset = fixed_offset_timezone
diff --git a/telegramer/include/pytz_deprecation_shim/_common.py b/telegramer/include/pytz_deprecation_shim/_common.py
new file mode 100644
index 0000000..ace322e
--- /dev/null
+++ b/telegramer/include/pytz_deprecation_shim/_common.py
@@ -0,0 +1,13 @@
+import sys
+
+_PYTZ_IMPORTED = False
+
+
+def pytz_imported():
+ """Detects whether or not pytz has been imported without importing pytz."""
+ global _PYTZ_IMPORTED
+
+ if not _PYTZ_IMPORTED and "pytz" in sys.modules:
+ _PYTZ_IMPORTED = True
+
+ return _PYTZ_IMPORTED
diff --git a/telegramer/include/pytz_deprecation_shim/_compat.py b/telegramer/include/pytz_deprecation_shim/_compat.py
new file mode 100644
index 0000000..5b73459
--- /dev/null
+++ b/telegramer/include/pytz_deprecation_shim/_compat.py
@@ -0,0 +1,15 @@
+import sys
+
+if sys.version_info[0] == 2:
+ from . import _compat_py2 as _compat_impl
+else:
+ from . import _compat_py3 as _compat_impl
+
+UTC = _compat_impl.UTC
+get_timezone = _compat_impl.get_timezone
+get_timezone_file = _compat_impl.get_timezone_file
+get_fixed_offset_zone = _compat_impl.get_fixed_offset_zone
+is_ambiguous = _compat_impl.is_ambiguous
+is_imaginary = _compat_impl.is_imaginary
+enfold = _compat_impl.enfold
+get_fold = _compat_impl.get_fold
diff --git a/telegramer/include/pytz_deprecation_shim/_compat_py2.py b/telegramer/include/pytz_deprecation_shim/_compat_py2.py
new file mode 100644
index 0000000..f473d26
--- /dev/null
+++ b/telegramer/include/pytz_deprecation_shim/_compat_py2.py
@@ -0,0 +1,43 @@
+from datetime import timedelta
+
+from dateutil import tz
+
+UTC = tz.UTC
+
+
+def get_timezone(key):
+ if not key:
+ raise KeyError("Unknown time zone: %s" % key)
+
+ try:
+ rv = tz.gettz(key)
+ except Exception:
+ rv = None
+
+ if rv is None or not isinstance(rv, (tz.tzutc, tz.tzfile)):
+ raise KeyError("Unknown time zone: %s" % key)
+
+ return rv
+
+
+def get_timezone_file(f, key=None):
+ return tz.tzfile(f)
+
+
+def get_fixed_offset_zone(offset):
+ return tz.tzoffset(None, timedelta(minutes=offset))
+
+
+def is_ambiguous(dt):
+ return tz.datetime_ambiguous(dt)
+
+
+def is_imaginary(dt):
+ return not tz.datetime_exists(dt)
+
+
+enfold = tz.enfold
+
+
+def get_fold(dt):
+ return getattr(dt, "fold", 0)
diff --git a/telegramer/include/pytz_deprecation_shim/_compat_py3.py b/telegramer/include/pytz_deprecation_shim/_compat_py3.py
new file mode 100644
index 0000000..8881aba
--- /dev/null
+++ b/telegramer/include/pytz_deprecation_shim/_compat_py3.py
@@ -0,0 +1,58 @@
+# Note: This file could use Python 3-only syntax, but at the moment this breaks
+# the coverage job on Python 2. Until we make it so that coverage can ignore
+# this file only on Python 2, we'll have to stick to 2/3-compatible syntax.
+try:
+ import zoneinfo
+except ImportError:
+ from backports import zoneinfo
+
+import datetime
+
+UTC = datetime.timezone.utc
+
+
+def get_timezone(key):
+ try:
+ return zoneinfo.ZoneInfo(key)
+ except (ValueError, OSError):
+ # TODO: Use `from e` when this file can use Python 3 syntax
+ raise KeyError(key)
+
+
+def get_timezone_file(f, key=None):
+ return zoneinfo.ZoneInfo.from_file(f, key=key)
+
+
+def get_fixed_offset_zone(offset):
+ return datetime.timezone(datetime.timedelta(minutes=offset))
+
+
+def is_imaginary(dt):
+ dt_rt = dt.astimezone(UTC).astimezone(dt.tzinfo)
+
+ return not (dt == dt_rt)
+
+
+def is_ambiguous(dt):
+ if is_imaginary(dt):
+ return False
+
+ wall_0 = dt
+ wall_1 = dt.replace(fold=not dt.fold)
+
+ # Ambiguous datetimes can only exist if the offset changes, so we don't
+ # actually have to check whether dst() or tzname() are different.
+ same_offset = wall_0.utcoffset() == wall_1.utcoffset()
+
+ return not same_offset
+
+
+def enfold(dt, fold=1):
+ if dt.fold != fold:
+ return dt.replace(fold=fold)
+ else:
+ return dt
+
+
+def get_fold(dt):
+ return dt.fold
diff --git a/telegramer/include/pytz_deprecation_shim/_exceptions.py b/telegramer/include/pytz_deprecation_shim/_exceptions.py
new file mode 100644
index 0000000..58d7af0
--- /dev/null
+++ b/telegramer/include/pytz_deprecation_shim/_exceptions.py
@@ -0,0 +1,75 @@
+from ._common import pytz_imported
+
+
+class PytzUsageWarning(RuntimeWarning):
+ """Warning raised when accessing features specific to ``pytz``'s interface.
+
+ This warning is used to direct users of ``pytz``-specific features like the
+ ``localize`` and ``normalize`` methods towards using the standard
+ ``tzinfo`` interface, so that these shims can be replaced with one of the
+ underlying libraries they are wrapping.
+ """
+
+
+class UnknownTimeZoneError(KeyError):
+ """Raised when no time zone is found for a specified key."""
+
+
+class InvalidTimeError(Exception):
+ """The base class for exceptions related to folds and gaps."""
+
+
+class AmbiguousTimeError(InvalidTimeError):
+ """Exception raised when ``is_dst=None`` for an ambiguous time (fold)."""
+
+
+class NonExistentTimeError(InvalidTimeError):
+ """Exception raised when ``is_dst=None`` for a non-existent time (gap)."""
+
+
+PYTZ_BASE_ERROR_MAPPING = {}
+
+
+def _make_pytz_derived_errors(
+ InvalidTimeError_=InvalidTimeError,
+ AmbiguousTimeError_=AmbiguousTimeError,
+ NonExistentTimeError_=NonExistentTimeError,
+ UnknownTimeZoneError_=UnknownTimeZoneError,
+):
+ if PYTZ_BASE_ERROR_MAPPING or not pytz_imported():
+ return
+
+ import pytz
+
+ class InvalidTimeError(InvalidTimeError_, pytz.InvalidTimeError):
+ pass
+
+ class AmbiguousTimeError(AmbiguousTimeError_, pytz.AmbiguousTimeError):
+ pass
+
+ class NonExistentTimeError(
+ NonExistentTimeError_, pytz.NonExistentTimeError
+ ):
+ pass
+
+ class UnknownTimeZoneError(
+ UnknownTimeZoneError_, pytz.UnknownTimeZoneError
+ ):
+ pass
+
+ PYTZ_BASE_ERROR_MAPPING.update(
+ {
+ InvalidTimeError_: InvalidTimeError,
+ AmbiguousTimeError_: AmbiguousTimeError,
+ NonExistentTimeError_: NonExistentTimeError,
+ UnknownTimeZoneError_: UnknownTimeZoneError,
+ }
+ )
+
+
+def get_exception(exc_type, msg):
+ _make_pytz_derived_errors()
+
+ out_exc_type = PYTZ_BASE_ERROR_MAPPING.get(exc_type, exc_type)
+
+ return out_exc_type(msg)
diff --git a/telegramer/include/pytz_deprecation_shim/_impl.py b/telegramer/include/pytz_deprecation_shim/_impl.py
new file mode 100644
index 0000000..5443047
--- /dev/null
+++ b/telegramer/include/pytz_deprecation_shim/_impl.py
@@ -0,0 +1,296 @@
+# -*- coding: utf-8 -*-
+import warnings
+from datetime import tzinfo
+
+from . import _compat
+from ._exceptions import (
+ AmbiguousTimeError,
+ NonExistentTimeError,
+ PytzUsageWarning,
+ UnknownTimeZoneError,
+ get_exception,
+)
+
+IS_DST_SENTINEL = object()
+KEY_SENTINEL = object()
+
+
+def timezone(key, _cache={}):
+ """Builds an IANA database time zone shim.
+
+ This is the equivalent of ``pytz.timezone``.
+
+ :param key:
+ A valid key from the IANA time zone database.
+
+ :raises UnknownTimeZoneError:
+ If an unknown value is passed, this will raise an exception that can be
+ caught by :exc:`pytz_deprecation_shim.UnknownTimeZoneError` or
+ ``pytz.UnknownTimeZoneError``. Like
+ :exc:`zoneinfo.ZoneInfoNotFoundError`, both of those are subclasses of
+ :exc:`KeyError`.
+ """
+ instance = _cache.get(key, None)
+ if instance is None:
+ if len(key) == 3 and key.lower() == "utc":
+ instance = _cache.setdefault(key, UTC)
+ else:
+ try:
+ zone = _compat.get_timezone(key)
+ except KeyError:
+ raise get_exception(UnknownTimeZoneError, key)
+ instance = _cache.setdefault(key, wrap_zone(zone, key=key))
+
+ return instance
+
+
+def fixed_offset_timezone(offset, _cache={}):
+ """Builds a fixed offset time zone shim.
+
+ This is the equivalent of ``pytz.FixedOffset``. An alias is available as
+ ``pytz_deprecation_shim.FixedOffset`` as well.
+
+ :param offset:
+ A fixed offset from UTC, in minutes. This must be in the range ``-1439
+ <= offset <= 1439``.
+
+ :raises ValueError:
+ For offsets whose absolute value is greater than or equal to 24 hours.
+
+ :return:
+ A shim time zone.
+ """
+ if not (-1440 < offset < 1440):
+ raise ValueError("absolute offset is too large", offset)
+
+ instance = _cache.get(offset, None)
+ if instance is None:
+ if offset == 0:
+ instance = _cache.setdefault(offset, UTC)
+ else:
+ zone = _compat.get_fixed_offset_zone(offset)
+ instance = _cache.setdefault(offset, wrap_zone(zone, key=None))
+
+ return instance
+
+
+def build_tzinfo(zone, fp):
+ """Builds a shim object from a TZif file.
+
+ This is a shim for ``pytz.build_tzinfo``. Given a value to use as the zone
+ IANA key and a file-like object containing a valid TZif file (i.e.
+ conforming to :rfc:`8536`), this builds a time zone object and wraps it in
+ a shim class.
+
+ The argument names are chosen to match those in ``pytz.build_tzinfo``.
+
+ :param zone:
+ A string to be used as the time zone object's IANA key.
+
+ :param fp:
+ A readable file-like object emitting bytes, pointing to a valid TZif
+ file.
+
+ :return:
+ A shim time zone.
+ """
+ zone_file = _compat.get_timezone_file(fp)
+
+ return wrap_zone(zone_file, key=zone)
+
+
+def wrap_zone(tz, key=KEY_SENTINEL, _cache={}):
+ """Wrap an existing time zone object in a shim class.
+
+ This is likely to be useful if you would like to work internally with
+ non-``pytz`` zones, but you expose an interface to callers relying on
+ ``pytz``'s interface. It may also be useful for passing non-``pytz`` zones
+ to libraries expecting to use ``pytz``'s interface.
+
+ :param tz:
+ A :pep:`495`-compatible time zone, such as those provided by
+ :mod:`dateutil.tz` or :mod:`zoneinfo`.
+
+ :param key:
+ The value for the IANA time zone key. This is optional for ``zoneinfo``
+ zones, but required for ``dateutil.tz`` zones.
+
+ :return:
+ A shim time zone.
+ """
+ if key is KEY_SENTINEL:
+ key = getattr(tz, "key", KEY_SENTINEL)
+
+ if key is KEY_SENTINEL:
+ raise TypeError(
+ "The `key` argument is required when wrapping zones that do not "
+ + "have a `key` attribute."
+ )
+
+ instance = _cache.get((id(tz), key), None)
+ if instance is None:
+ instance = _cache.setdefault((id(tz), key), _PytzShimTimezone(tz, key))
+
+ return instance
+
+
+class _PytzShimTimezone(tzinfo):
+ # Add instance variables for _zone and _key because this will make error
+ # reporting with partially-initialized _BasePytzShimTimezone objects
+ # work better.
+ _zone = None
+ _key = None
+
+ def __init__(self, zone, key):
+ self._key = key
+ self._zone = zone
+
+ def utcoffset(self, dt):
+ return self._zone.utcoffset(dt)
+
+ def dst(self, dt):
+ return self._zone.dst(dt)
+
+ def tzname(self, dt):
+ return self._zone.tzname(dt)
+
+ def fromutc(self, dt):
+ # The default fromutc implementation only works if tzinfo is "self"
+ dt_base = dt.replace(tzinfo=self._zone)
+ dt_out = self._zone.fromutc(dt_base)
+
+ return dt_out.replace(tzinfo=self)
+
+ def __str__(self):
+ if self._key is not None:
+ return str(self._key)
+ else:
+ return repr(self)
+
+ def __repr__(self):
+ return "%s(%s, %s)" % (
+ self.__class__.__name__,
+ repr(self._zone),
+ repr(self._key),
+ )
+
+ def unwrap_shim(self):
+ """Returns the underlying class that the shim is a wrapper for.
+
+ This is a shim-specific method equivalent to
+ :func:`pytz_deprecation_shim.helpers.upgrade_tzinfo`. It is provided as
+ a method to allow end-users to upgrade shim timezones without requiring
+ an explicit dependency on ``pytz_deprecation_shim``, e.g.:
+
+ .. code-block:: python
+
+ if getattr(tz, "unwrap_shim", None) is None:
+ tz = tz.unwrap_shim()
+ """
+ return self._zone
+
+ @property
+ def zone(self):
+ warnings.warn(
+ "The zone attribute is specific to pytz's interface; "
+ + "please migrate to a new time zone provider. "
+ + "For more details on how to do so, see %s"
+ % PYTZ_MIGRATION_GUIDE_URL,
+ PytzUsageWarning,
+ stacklevel=2,
+ )
+
+ return self._key
+
+ def localize(self, dt, is_dst=IS_DST_SENTINEL):
+ warnings.warn(
+ "The localize method is no longer necessary, as this "
+ + "time zone supports the fold attribute (PEP 495). "
+ + "For more details on migrating to a PEP 495-compliant "
+ + "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL,
+ PytzUsageWarning,
+ stacklevel=2,
+ )
+
+ if dt.tzinfo is not None:
+ raise ValueError("Not naive datetime (tzinfo is already set)")
+
+ dt_out = dt.replace(tzinfo=self)
+
+ if is_dst is IS_DST_SENTINEL:
+ return dt_out
+
+ dt_ambiguous = _compat.is_ambiguous(dt_out)
+ dt_imaginary = (
+ _compat.is_imaginary(dt_out) if not dt_ambiguous else False
+ )
+
+ if is_dst is None:
+ if dt_imaginary:
+ raise get_exception(
+ NonExistentTimeError, dt.replace(tzinfo=None)
+ )
+
+ if dt_ambiguous:
+ raise get_exception(AmbiguousTimeError, dt.replace(tzinfo=None))
+
+ elif dt_ambiguous or dt_imaginary:
+ # Start by normalizing the folds; dt_out may have fold=0 or fold=1,
+ # but we need to know the DST offset on both sides anyway, so we
+ # will get one datetime representing each side of the fold, then
+ # decide which one we're going to return.
+ if _compat.get_fold(dt_out):
+ dt_enfolded = dt_out
+ dt_out = _compat.enfold(dt_out, fold=0)
+ else:
+ dt_enfolded = _compat.enfold(dt_out, fold=1)
+
+ # Now we want to decide whether the fold=0 or fold=1 represents
+ # what pytz would return for `is_dst=True`
+ enfolded_dst = bool(dt_enfolded.dst())
+ if bool(dt_out.dst()) == enfolded_dst:
+ # If this is not a transition between standard time and
+ # daylight saving time, pytz will consider the larger offset
+ # the DST offset.
+ enfolded_dst = dt_enfolded.utcoffset() > dt_out.utcoffset()
+
+ # The default we've established is that dt_out is fold=0; swap it
+ # for the fold=1 datetime if is_dst == True and the enfolded side
+ # is DST or if is_dst == False and the enfolded side is *not* DST.
+ if is_dst == enfolded_dst:
+ dt_out = dt_enfolded
+
+ return dt_out
+
+ def normalize(self, dt):
+ warnings.warn(
+ "The normalize method is no longer necessary, as this "
+ + "time zone supports the fold attribute (PEP 495). "
+ + "For more details on migrating to a PEP 495-compliant "
+ + "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL,
+ PytzUsageWarning,
+ stacklevel=2,
+ )
+
+ if dt.tzinfo is None:
+ raise ValueError("Naive time - no tzinfo set")
+
+ if dt.tzinfo is self:
+ return dt
+
+ return dt.astimezone(self)
+
+ def __copy__(self):
+ return self
+
+ def __deepcopy__(self, memo=None):
+ return self
+
+ def __reduce__(self):
+ return wrap_zone, (self._zone, self._key)
+
+
+UTC = wrap_zone(_compat.UTC, "UTC")
+PYTZ_MIGRATION_GUIDE_URL = (
+ "https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html"
+)
diff --git a/telegramer/include/pytz_deprecation_shim/helpers.py b/telegramer/include/pytz_deprecation_shim/helpers.py
new file mode 100644
index 0000000..6b05b13
--- /dev/null
+++ b/telegramer/include/pytz_deprecation_shim/helpers.py
@@ -0,0 +1,90 @@
+"""
+This module contains helper functions to ease the transition from ``pytz`` to
+another :pep:`495`-compatible library.
+"""
+from . import _common, _compat
+from ._impl import _PytzShimTimezone
+
+_PYTZ_BASE_CLASSES = None
+
+
+def is_pytz_zone(tz):
+ """Check if a time zone is a ``pytz`` time zone.
+
+ This will only import ``pytz`` if it has already been imported, and does
+ not rely on the existence of the ``localize`` or ``normalize`` methods
+ (since the shim classes also have these methods, but are not ``pytz``
+ zones).
+ """
+
+ # If pytz is not in sys.modules, then we will assume the time zone is not a
+ # pytz zone. It is possible that someone has manipulated sys.modules to
+ # remove pytz, but that's the kind of thing that causes all kinds of other
+ # problems anyway, so we'll call that an unsupported configuration.
+ if not _common.pytz_imported():
+ return False
+
+ if _PYTZ_BASE_CLASSES is None:
+ _populate_pytz_base_classes()
+
+ return isinstance(tz, _PYTZ_BASE_CLASSES)
+
+
+def upgrade_tzinfo(tz):
+ """Convert a ``pytz`` or shim timezone into its modern equivalent.
+
+ The shim classes are thin wrappers around :mod:`zoneinfo` or
+ :mod:`dateutil.tz` implementations of the :class:`datetime.tzinfo` base
+ class. This function removes the shim and returns the underlying "upgraded"
+ time zone.
+
+ When passed a ``pytz`` zone (not a shim), this returns the non-``pytz``
+ equivalent. This may fail if ``pytz`` is using a data source incompatible
+ with the upgraded provider's data source, or if the ``pytz`` zone was built
+ from a file rather than an IANA key.
+
+ When passed an object that is not a shim or a ``pytz`` zone, this returns
+ the original object.
+
+ :param tz:
+ A :class:`datetime.tzinfo` object.
+
+ :raises KeyError:
+ If a ``pytz`` zone is passed to the function with no equivalent in the
+ :pep:`495`-compatible library's version of the Olson database.
+
+ :return:
+ A :pep:`495`-compatible equivalent of any ``pytz`` or shim
+ class, or the original object.
+ """
+ if isinstance(tz, _PytzShimTimezone):
+ return tz._zone
+
+ if is_pytz_zone(tz):
+ if tz.zone is None:
+ # This is a fixed offset zone
+ offset = tz.utcoffset(None)
+ offset_minutes = offset.total_seconds() / 60
+
+ return _compat.get_fixed_offset_zone(offset_minutes)
+
+ if tz.zone == "UTC":
+ return _compat.UTC
+
+ return _compat.get_timezone(tz.zone)
+
+ return tz
+
+
+def _populate_pytz_base_classes():
+ import pytz
+ from pytz.tzinfo import BaseTzInfo
+
+ base_classes = (BaseTzInfo, pytz._FixedOffset)
+
+ # In releases prior to 2018.4, pytz.UTC was not a subclass of BaseTzInfo
+ if not isinstance(pytz.UTC, BaseTzInfo): # pragma: nocover
+ base_classes = base_classes + (type(pytz.UTC),)
+
+ global _PYTZ_BASE_CLASSES
+ _PYTZ_BASE_CLASSES = base_classes
diff --git a/telegramer/include/socks.py b/telegramer/include/socks.py
index a2fe8a3..5092846 100644
--- a/telegramer/include/socks.py
+++ b/telegramer/include/socks.py
@@ -86,7 +86,7 @@
PROXY_TYPE_HTTP = HTTP = 3
PROXY_TYPES = {"SOCKS4": SOCKS4, "SOCKS5": SOCKS5, "HTTP": HTTP}
-PRINTABLE_PROXY_TYPES = dict(zip(PROXY_TYPES.values(), PROXY_TYPES.keys()))
+PRINTABLE_PROXY_TYPES = dict(list(zip(list(PROXY_TYPES.values()), list(PROXY_TYPES.keys()))))
_orgsocket = _orig_socket = socket.socket
diff --git a/telegramer/include/sockshandler.py b/telegramer/include/sockshandler.py
index 26c8343..b9b2b67 100644
--- a/telegramer/include/sockshandler.py
+++ b/telegramer/include/sockshandler.py
@@ -10,23 +10,23 @@
import ssl
try:
- import urllib2
- import httplib
+ import urllib.request, urllib.error, urllib.parse
+ import http.client
except ImportError: # Python 3
import urllib.request as urllib2
- import http.client as httplib
+ from http import client as httplib
-import socks # $ pip install PySocks
+from . import socks # $ pip install PySocks
def merge_dict(a, b):
d = a.copy()
d.update(b)
return d
-class SocksiPyConnection(httplib.HTTPConnection):
+class SocksiPyConnection(http.client.HTTPConnection):
def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs):
self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password)
- httplib.HTTPConnection.__init__(self, *args, **kwargs)
+ http.client.HTTPConnection.__init__(self, *args, **kwargs)
def connect(self):
self.sock = socks.socksocket()
@@ -35,10 +35,10 @@ def connect(self):
self.sock.settimeout(self.timeout)
self.sock.connect((self.host, self.port))
-class SocksiPyConnectionS(httplib.HTTPSConnection):
+class SocksiPyConnectionS(http.client.HTTPSConnection):
def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs):
self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password)
- httplib.HTTPSConnection.__init__(self, *args, **kwargs)
+ http.client.HTTPSConnection.__init__(self, *args, **kwargs)
def connect(self):
sock = socks.socksocket()
@@ -48,11 +48,11 @@ def connect(self):
sock.connect((self.host, self.port))
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file)
-class SocksiPyHandler(urllib2.HTTPHandler, urllib2.HTTPSHandler):
+class SocksiPyHandler(urllib.request.HTTPHandler, urllib.request.HTTPSHandler):
def __init__(self, *args, **kwargs):
self.args = args
self.kw = kwargs
- urllib2.HTTPHandler.__init__(self)
+ urllib.request.HTTPHandler.__init__(self)
def http_open(self, req):
def build(host, port=None, timeout=0, **kwargs):
@@ -74,6 +74,6 @@ def build(host, port=None, timeout=0, **kwargs):
port = int(sys.argv[1])
except (ValueError, IndexError):
port = 9050
- opener = urllib2.build_opener(SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, "localhost", port))
- print("HTTP: " + opener.open("http://httpbin.org/ip").read().decode())
- print("HTTPS: " + opener.open("https://httpbin.org/ip").read().decode())
+ opener = urllib.request.build_opener(SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, "localhost", port))
+ print(("HTTP: " + opener.open("http://httpbin.org/ip").read().decode()))
+ print(("HTTPS: " + opener.open("https://httpbin.org/ip").read().decode()))
diff --git a/telegramer/include/telegram/__init__.py b/telegramer/include/telegram/__init__.py
index cf45c69..c0b36de 100644
--- a/telegramer/include/telegram/__init__.py
+++ b/telegramer/include/telegram/__init__.py
@@ -1,7 +1,7 @@
-''#!/usr/bin/env python
+#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,10 +19,24 @@
"""A library that provides a Python interface to the Telegram Bot API"""
from .base import TelegramObject
+from .botcommand import BotCommand
from .user import User
from .files.chatphoto import ChatPhoto
from .chat import Chat
-from .chatmember import ChatMember
+from .chatlocation import ChatLocation
+from .chatinvitelink import ChatInviteLink
+from .chatjoinrequest import ChatJoinRequest
+from .chatmember import (
+ ChatMember,
+ ChatMemberOwner,
+ ChatMemberAdministrator,
+ ChatMemberMember,
+ ChatMemberRestricted,
+ ChatMemberLeft,
+ ChatMemberBanned,
+)
+from .chatmemberupdated import ChatMemberUpdated
+from .chatpermissions import ChatPermissions
from .files.photosize import PhotoSize
from .files.audio import Audio
from .files.voice import Voice
@@ -35,7 +49,9 @@
from .files.venue import Venue
from .files.videonote import VideoNote
from .chataction import ChatAction
+from .dice import Dice
from .userprofilephotos import UserProfilePhotos
+from .keyboardbuttonpolltype import KeyboardButtonPollType
from .keyboardbutton import KeyboardButton
from .replymarkup import ReplyMarkup
from .replykeyboardmarkup import ReplyKeyboardMarkup
@@ -46,7 +62,17 @@
from .files.file import File
from .parsemode import ParseMode
from .messageentity import MessageEntity
+from .messageid import MessageId
from .games.game import Game
+from .poll import Poll, PollOption, PollAnswer
+from .voicechat import (
+ VoiceChatStarted,
+ VoiceChatEnded,
+ VoiceChatParticipantsInvited,
+ VoiceChatScheduled,
+)
+from .loginurl import LoginUrl
+from .proximityalerttriggered import ProximityAlertTriggered
from .games.callbackgame import CallbackGame
from .payment.shippingaddress import ShippingAddress
from .payment.orderinfo import OrderInfo
@@ -57,11 +83,12 @@
from .passport.data import IdDocumentData, PersonalDetails, ResidentialAddress
from .passport.encryptedpassportelement import EncryptedPassportElement
from .passport.passportdata import PassportData
+from .inline.inlinekeyboardbutton import InlineKeyboardButton
+from .inline.inlinekeyboardmarkup import InlineKeyboardMarkup
+from .messageautodeletetimerchanged import MessageAutoDeleteTimerChanged
from .message import Message
from .callbackquery import CallbackQuery
from .choseninlineresult import ChosenInlineResult
-from .inline.inlinekeyboardbutton import InlineKeyboardButton
-from .inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from .inline.inputmessagecontent import InputMessageContent
from .inline.inlinequery import InlineQuery
from .inline.inlinequeryresult import InlineQueryResult
@@ -88,69 +115,214 @@
from .inline.inputtextmessagecontent import InputTextMessageContent
from .inline.inputlocationmessagecontent import InputLocationMessageContent
from .inline.inputvenuemessagecontent import InputVenueMessageContent
-from .inline.inputcontactmessagecontent import InputContactMessageContent
from .payment.labeledprice import LabeledPrice
+from .inline.inputinvoicemessagecontent import InputInvoiceMessageContent
+from .inline.inputcontactmessagecontent import InputContactMessageContent
from .payment.shippingoption import ShippingOption
from .payment.precheckoutquery import PreCheckoutQuery
from .payment.shippingquery import ShippingQuery
from .webhookinfo import WebhookInfo
from .games.gamehighscore import GameHighScore
from .update import Update
-from .files.inputmedia import (InputMedia, InputMediaVideo, InputMediaPhoto, InputMediaAnimation,
- InputMediaAudio, InputMediaDocument)
+from .files.inputmedia import (
+ InputMedia,
+ InputMediaVideo,
+ InputMediaPhoto,
+ InputMediaAnimation,
+ InputMediaAudio,
+ InputMediaDocument,
+)
+from .constants import (
+ MAX_MESSAGE_LENGTH,
+ MAX_CAPTION_LENGTH,
+ SUPPORTED_WEBHOOK_PORTS,
+ MAX_FILESIZE_DOWNLOAD,
+ MAX_FILESIZE_UPLOAD,
+ MAX_MESSAGES_PER_SECOND_PER_CHAT,
+ MAX_MESSAGES_PER_SECOND,
+ MAX_MESSAGES_PER_MINUTE_PER_GROUP,
+)
+from .passport.passportelementerrors import (
+ PassportElementError,
+ PassportElementErrorDataField,
+ PassportElementErrorFile,
+ PassportElementErrorFiles,
+ PassportElementErrorFrontSide,
+ PassportElementErrorReverseSide,
+ PassportElementErrorSelfie,
+ PassportElementErrorTranslationFile,
+ PassportElementErrorTranslationFiles,
+ PassportElementErrorUnspecified,
+)
+from .passport.credentials import (
+ Credentials,
+ DataCredentials,
+ SecureData,
+ SecureValue,
+ FileCredentials,
+ TelegramDecryptionError,
+)
+from .botcommandscope import (
+ BotCommandScope,
+ BotCommandScopeDefault,
+ BotCommandScopeAllPrivateChats,
+ BotCommandScopeAllGroupChats,
+ BotCommandScopeAllChatAdministrators,
+ BotCommandScopeChat,
+ BotCommandScopeChatAdministrators,
+ BotCommandScopeChatMember,
+)
from .bot import Bot
-from .constants import (MAX_MESSAGE_LENGTH, MAX_CAPTION_LENGTH, SUPPORTED_WEBHOOK_PORTS,
- MAX_FILESIZE_DOWNLOAD, MAX_FILESIZE_UPLOAD,
- MAX_MESSAGES_PER_SECOND_PER_CHAT, MAX_MESSAGES_PER_SECOND,
- MAX_MESSAGES_PER_MINUTE_PER_GROUP)
-from .passport.passportelementerrors import (PassportElementError,
- PassportElementErrorDataField,
- PassportElementErrorFile,
- PassportElementErrorFiles,
- PassportElementErrorFrontSide,
- PassportElementErrorReverseSide,
- PassportElementErrorSelfie,
- PassportElementErrorTranslationFile,
- PassportElementErrorTranslationFiles,
- PassportElementErrorUnspecified)
-from .passport.credentials import (Credentials,
- DataCredentials,
- SecureData,
- FileCredentials,
- TelegramDecryptionError)
-from .version import __version__ # flake8: noqa
+from .version import __version__, bot_api_version # noqa: F401
__author__ = 'devs@python-telegram-bot.org'
-__all__ = [
- 'Audio', 'Bot', 'Chat', 'ChatMember', 'ChatAction', 'ChosenInlineResult', 'CallbackQuery',
- 'Contact', 'Document', 'File', 'ForceReply', 'InlineKeyboardButton',
- 'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', 'InlineQueryResult',
- 'InlineQueryResultArticle', 'InlineQueryResultAudio', 'InlineQueryResultCachedAudio',
- 'InlineQueryResultCachedDocument', 'InlineQueryResultCachedGif',
- 'InlineQueryResultCachedMpeg4Gif', 'InlineQueryResultCachedPhoto',
- 'InlineQueryResultCachedSticker', 'InlineQueryResultCachedVideo',
- 'InlineQueryResultCachedVoice', 'InlineQueryResultContact', 'InlineQueryResultDocument',
- 'InlineQueryResultGif', 'InlineQueryResultLocation', 'InlineQueryResultMpeg4Gif',
- 'InlineQueryResultPhoto', 'InlineQueryResultVenue', 'InlineQueryResultVideo',
- 'InlineQueryResultVoice', 'InlineQueryResultGame', 'InputContactMessageContent', 'InputFile',
- 'InputLocationMessageContent', 'InputMessageContent', 'InputTextMessageContent',
- 'InputVenueMessageContent', 'KeyboardButton', 'Location', 'EncryptedCredentials',
- 'PassportFile', 'EncryptedPassportElement', 'PassportData', 'Message', 'MessageEntity',
- 'ParseMode', 'PhotoSize', 'ReplyKeyboardRemove', 'ReplyKeyboardMarkup', 'ReplyMarkup',
- 'Sticker', 'TelegramError', 'TelegramObject', 'Update', 'User', 'UserProfilePhotos', 'Venue',
- 'Video', 'Voice', 'MAX_MESSAGE_LENGTH', 'MAX_CAPTION_LENGTH', 'SUPPORTED_WEBHOOK_PORTS',
- 'MAX_FILESIZE_DOWNLOAD', 'MAX_FILESIZE_UPLOAD', 'MAX_MESSAGES_PER_SECOND_PER_CHAT',
- 'MAX_MESSAGES_PER_SECOND', 'MAX_MESSAGES_PER_MINUTE_PER_GROUP', 'WebhookInfo', 'Animation',
- 'Game', 'GameHighScore', 'VideoNote', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption',
- 'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery', 'ChatPhoto',
- 'StickerSet', 'MaskPosition', 'CallbackGame', 'InputMedia', 'InputMediaPhoto',
- 'InputMediaVideo', 'PassportElementError', 'PassportElementErrorFile',
- 'PassportElementErrorReverseSide', 'PassportElementErrorFrontSide',
- 'PassportElementErrorFiles', 'PassportElementErrorDataField', 'PassportElementErrorFile',
- 'Credentials', 'DataCredentials', 'SecureData', 'FileCredentials', 'IdDocumentData',
- 'PersonalDetails', 'ResidentialAddress', 'InputMediaVideo', 'InputMediaAnimation',
- 'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError',
- 'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile',
- 'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified'
-]
+__all__ = ( # Keep this alphabetically ordered
+ 'Animation',
+ 'Audio',
+ 'Bot',
+ 'BotCommand',
+ 'BotCommandScope',
+ 'BotCommandScopeAllChatAdministrators',
+ 'BotCommandScopeAllGroupChats',
+ 'BotCommandScopeAllPrivateChats',
+ 'BotCommandScopeChat',
+ 'BotCommandScopeChatAdministrators',
+ 'BotCommandScopeChatMember',
+ 'BotCommandScopeDefault',
+ 'CallbackGame',
+ 'CallbackQuery',
+ 'Chat',
+ 'ChatAction',
+ 'ChatInviteLink',
+ 'ChatJoinRequest',
+ 'ChatLocation',
+ 'ChatMember',
+ 'ChatMemberOwner',
+ 'ChatMemberAdministrator',
+ 'ChatMemberMember',
+ 'ChatMemberRestricted',
+ 'ChatMemberLeft',
+ 'ChatMemberBanned',
+ 'ChatMemberUpdated',
+ 'ChatPermissions',
+ 'ChatPhoto',
+ 'ChosenInlineResult',
+ 'Contact',
+ 'Credentials',
+ 'DataCredentials',
+ 'Dice',
+ 'Document',
+ 'EncryptedCredentials',
+ 'EncryptedPassportElement',
+ 'File',
+ 'FileCredentials',
+ 'ForceReply',
+ 'Game',
+ 'GameHighScore',
+ 'IdDocumentData',
+ 'InlineKeyboardButton',
+ 'InlineKeyboardMarkup',
+ 'InlineQuery',
+ 'InlineQueryResult',
+ 'InlineQueryResultArticle',
+ 'InlineQueryResultAudio',
+ 'InlineQueryResultCachedAudio',
+ 'InlineQueryResultCachedDocument',
+ 'InlineQueryResultCachedGif',
+ 'InlineQueryResultCachedMpeg4Gif',
+ 'InlineQueryResultCachedPhoto',
+ 'InlineQueryResultCachedSticker',
+ 'InlineQueryResultCachedVideo',
+ 'InlineQueryResultCachedVoice',
+ 'InlineQueryResultContact',
+ 'InlineQueryResultDocument',
+ 'InlineQueryResultGame',
+ 'InlineQueryResultGif',
+ 'InlineQueryResultLocation',
+ 'InlineQueryResultMpeg4Gif',
+ 'InlineQueryResultPhoto',
+ 'InlineQueryResultVenue',
+ 'InlineQueryResultVideo',
+ 'InlineQueryResultVoice',
+ 'InputContactMessageContent',
+ 'InputFile',
+ 'InputInvoiceMessageContent',
+ 'InputLocationMessageContent',
+ 'InputMedia',
+ 'InputMediaAnimation',
+ 'InputMediaAudio',
+ 'InputMediaDocument',
+ 'InputMediaPhoto',
+ 'InputMediaVideo',
+ 'InputMessageContent',
+ 'InputTextMessageContent',
+ 'InputVenueMessageContent',
+ 'Invoice',
+ 'KeyboardButton',
+ 'KeyboardButtonPollType',
+ 'LabeledPrice',
+ 'Location',
+ 'LoginUrl',
+ 'MAX_CAPTION_LENGTH',
+ 'MAX_FILESIZE_DOWNLOAD',
+ 'MAX_FILESIZE_UPLOAD',
+ 'MAX_MESSAGES_PER_MINUTE_PER_GROUP',
+ 'MAX_MESSAGES_PER_SECOND',
+ 'MAX_MESSAGES_PER_SECOND_PER_CHAT',
+ 'MAX_MESSAGE_LENGTH',
+ 'MaskPosition',
+ 'Message',
+ 'MessageAutoDeleteTimerChanged',
+ 'MessageEntity',
+ 'MessageId',
+ 'OrderInfo',
+ 'ParseMode',
+ 'PassportData',
+ 'PassportElementError',
+ 'PassportElementErrorDataField',
+ 'PassportElementErrorFile',
+ 'PassportElementErrorFiles',
+ 'PassportElementErrorFrontSide',
+ 'PassportElementErrorReverseSide',
+ 'PassportElementErrorSelfie',
+ 'PassportElementErrorTranslationFile',
+ 'PassportElementErrorTranslationFiles',
+ 'PassportElementErrorUnspecified',
+ 'PassportFile',
+ 'PersonalDetails',
+ 'PhotoSize',
+ 'Poll',
+ 'PollAnswer',
+ 'PollOption',
+ 'PreCheckoutQuery',
+ 'ProximityAlertTriggered',
+ 'ReplyKeyboardMarkup',
+ 'ReplyKeyboardRemove',
+ 'ReplyMarkup',
+ 'ResidentialAddress',
+ 'SUPPORTED_WEBHOOK_PORTS',
+ 'SecureData',
+ 'SecureValue',
+ 'ShippingAddress',
+ 'ShippingOption',
+ 'ShippingQuery',
+ 'Sticker',
+ 'StickerSet',
+ 'SuccessfulPayment',
+ 'TelegramDecryptionError',
+ 'TelegramError',
+ 'TelegramObject',
+ 'Update',
+ 'User',
+ 'UserProfilePhotos',
+ 'Venue',
+ 'Video',
+ 'VideoNote',
+ 'Voice',
+ 'VoiceChatStarted',
+ 'VoiceChatEnded',
+ 'VoiceChatScheduled',
+ 'VoiceChatParticipantsInvited',
+ 'WebhookInfo',
+)
diff --git a/telegramer/include/telegram/__main__.py b/telegramer/include/telegram/__main__.py
index ad1db8d..2e7a7de 100644
--- a/telegramer/include/telegram/__main__.py
+++ b/telegramer/include/telegram/__main__.py
@@ -1,7 +1,7 @@
# !/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,22 +16,37 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=C0114
+import subprocess
import sys
+from typing import Optional
import certifi
-import future
from . import __version__ as telegram_ver
+from .constants import BOT_API_VERSION
-def print_ver_info():
- print('python-telegram-bot {0}'.format(telegram_ver))
- print('certifi {0}'.format(certifi.__version__))
- print('future {0}'.format(future.__version__))
- print('Python {0}'.format(sys.version.replace('\n', ' ')))
+def _git_revision() -> Optional[str]:
+ try:
+ output = subprocess.check_output( # skipcq: BAN-B607
+ ["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT
+ )
+ except (subprocess.SubprocessError, OSError):
+ return None
+ return output.decode().strip()
-def main():
+def print_ver_info() -> None: # skipcq: PY-D0003
+ git_revision = _git_revision()
+ print(f'python-telegram-bot {telegram_ver}' + (f' ({git_revision})' if git_revision else ''))
+ print(f'Bot API {BOT_API_VERSION}')
+ print(f'certifi {certifi.__version__}') # type: ignore[attr-defined]
+ sys_version = sys.version.replace('\n', ' ')
+ print(f'Python {sys_version}')
+
+
+def main() -> None: # skipcq: PY-D0003
print_ver_info()
diff --git a/telegramer/include/telegram/base.py b/telegramer/include/telegram/base.py
index 80259ac..f119d96 100644
--- a/telegramer/include/telegram/base.py
+++ b/telegramer/include/telegram/base.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,58 +17,110 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram Objects."""
-
try:
import ujson as json
except ImportError:
- import json
+ import json # type: ignore[no-redef]
+
+import warnings
+from typing import TYPE_CHECKING, List, Optional, Tuple, Type, TypeVar
+
+from telegram.utils.types import JSONDict
+from telegram.utils.deprecate import set_new_attribute_deprecated
+
+if TYPE_CHECKING:
+ from telegram import Bot
-from abc import ABCMeta
+TO = TypeVar('TO', bound='TelegramObject', covariant=True)
-class TelegramObject(object):
- """Base class for most telegram objects."""
+class TelegramObject:
+ """Base class for most Telegram objects."""
- __metaclass__ = ABCMeta
- _id_attrs = ()
+ _id_attrs: Tuple[object, ...] = ()
- def __str__(self):
+ # Adding slots reduces memory usage & allows for faster attribute access.
+ # Only instance variables should be added to __slots__.
+ # We add __dict__ here for backward compatibility & also to avoid repetition for subclasses.
+ __slots__ = ('__dict__',)
+
+ def __str__(self) -> str:
return str(self.to_dict())
- def __getitem__(self, item):
- return self.__dict__[item]
+ def __getitem__(self, item: str) -> object:
+ return getattr(self, item, None)
+
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
+
+ @staticmethod
+ def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]:
+ return None if data is None else data.copy()
@classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
+ def de_json(cls: Type[TO], data: Optional[JSONDict], bot: 'Bot') -> Optional[TO]:
+ """Converts JSON data to a Telegram object.
- data = data.copy()
+ Args:
+ data (Dict[:obj:`str`, ...]): The JSON data.
+ bot (:class:`telegram.Bot`): The bot associated with this object.
- return data
+ Returns:
+ The Telegram object.
- def to_json(self):
"""
+ data = cls._parse_data(data)
+
+ if data is None:
+ return None
+
+ if cls == TelegramObject:
+ return cls()
+ return cls(bot=bot, **data) # type: ignore[call-arg]
+
+ @classmethod
+ def de_list(cls: Type[TO], data: Optional[List[JSONDict]], bot: 'Bot') -> List[Optional[TO]]:
+ """Converts JSON data to a list of Telegram objects.
+
+ Args:
+ data (Dict[:obj:`str`, ...]): The JSON data.
+ bot (:class:`telegram.Bot`): The bot associated with these objects.
+
Returns:
- :obj:`str`
+ A list of Telegram objects.
"""
+ if not data:
+ return []
+
+ return [cls.de_json(d, bot) for d in data]
+ def to_json(self) -> str:
+ """Gives a JSON representation of object.
+
+ Returns:
+ :obj:`str`
+ """
return json.dumps(self.to_dict())
- def to_dict(self):
- data = dict()
+ def to_dict(self) -> JSONDict:
+ """Gives representation of object as :obj:`dict`.
- for key in iter(self.__dict__):
- if key in ('bot',
- '_id_attrs',
- '_credentials',
- '_decrypted_credentials',
- '_decrypted_data',
- '_decrypted_secret'):
+ Returns:
+ :obj:`dict`
+ """
+ data = {}
+
+ # We want to get all attributes for the class, using self.__slots__ only includes the
+ # attributes used by that class itself, and not its superclass(es). Hence we get its MRO
+ # and then get their attributes. The `[:-2]` slice excludes the `object` class & the
+ # TelegramObject class itself.
+ attrs = {attr for cls in self.__class__.__mro__[:-2] for attr in cls.__slots__}
+ for key in attrs:
+ if key == 'bot' or key.startswith('_'):
continue
- value = self.__dict__[key]
+ value = getattr(self, key, None)
if value is not None:
if hasattr(value, 'to_dict'):
data[key] = value.to_dict()
@@ -79,12 +131,22 @@ def to_dict(self):
data['from'] = data.pop('from_user', None)
return data
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
if isinstance(other, self.__class__):
+ if self._id_attrs == ():
+ warnings.warn(
+ f"Objects of type {self.__class__.__name__} can not be meaningfully tested for"
+ " equivalence."
+ )
+ if other._id_attrs == ():
+ warnings.warn(
+ f"Objects of type {other.__class__.__name__} can not be meaningfully tested"
+ " for equivalence."
+ )
return self._id_attrs == other._id_attrs
- return super(TelegramObject, self).__eq__(other) # pylint: disable=no-member
+ return super().__eq__(other) # pylint: disable=no-member
- def __hash__(self):
+ def __hash__(self) -> int:
if self._id_attrs:
return hash((self.__class__, self._id_attrs)) # pylint: disable=no-member
- return super(TelegramObject, self).__hash__()
+ return super().__hash__()
diff --git a/telegramer/include/telegram/bot.py b/telegramer/include/telegram/bot.py
index dbdd8df..cc983d5 100644
--- a/telegramer/include/telegram/bot.py
+++ b/telegramer/include/telegram/bot.py
@@ -1,9 +1,8 @@
#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# pylint: disable=E0611,E0213,E1102,C0103,E1101,W0613,R0913,R0904
+# pylint: disable=E0611,E0213,E1102,E1101,R0913,R0904
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -21,58 +20,114 @@
"""This module contains an object that represents a Telegram Bot."""
import functools
-try:
- import ujson as json
-except ImportError:
- import json
import logging
import warnings
from datetime import datetime
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives import serialization
-# REMREM from future.utils import string_types
-try:
- from future.utils import string_types
-except Exception as e:
- pass
+from typing import (
+ TYPE_CHECKING,
+ Callable,
+ List,
+ Optional,
+ Tuple,
+ TypeVar,
+ Union,
+ no_type_check,
+ Dict,
+ cast,
+ Sequence,
+)
try:
- string_types
-except NameError:
- string_types = str
+ import ujson as json
+except ImportError:
+ import json # type: ignore[no-redef] # noqa: F723
+try:
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import serialization
-from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File,
- ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore, StickerSet,
- PhotoSize, Audio, Document, Sticker, Video, Animation, Voice, VideoNote,
- Location, Venue, Contact, InputFile)
+ CRYPTO_INSTALLED = True
+except ImportError:
+ default_backend = None # type: ignore[assignment]
+ serialization = None # type: ignore[assignment]
+ CRYPTO_INSTALLED = False
+
+from telegram import (
+ Animation,
+ Audio,
+ BotCommand,
+ BotCommandScope,
+ Chat,
+ ChatMember,
+ ChatPermissions,
+ ChatPhoto,
+ Contact,
+ Document,
+ File,
+ GameHighScore,
+ Location,
+ MaskPosition,
+ Message,
+ MessageId,
+ PassportElementError,
+ PhotoSize,
+ Poll,
+ ReplyMarkup,
+ ShippingOption,
+ Sticker,
+ StickerSet,
+ TelegramObject,
+ Update,
+ User,
+ UserProfilePhotos,
+ Venue,
+ Video,
+ VideoNote,
+ Voice,
+ WebhookInfo,
+ InlineKeyboardMarkup,
+ ChatInviteLink,
+)
+from telegram.constants import MAX_INLINE_QUERY_RESULTS
from telegram.error import InvalidToken, TelegramError
-from telegram.utils.helpers import to_timestamp
+from telegram.utils.deprecate import TelegramDeprecationWarning
+from telegram.utils.helpers import (
+ DEFAULT_NONE,
+ DefaultValue,
+ to_timestamp,
+ is_local_file,
+ parse_file_input,
+ DEFAULT_20,
+)
from telegram.utils.request import Request
-
-logging.getLogger(__name__).addHandler(logging.NullHandler())
-
-
-def info(func):
- @functools.wraps(func)
- def decorator(self, *args, **kwargs):
- if not self.bot:
- self.get_me()
-
- result = func(self, *args, **kwargs)
- return result
-
- return decorator
-
-
-def log(func):
+from telegram.utils.types import FileInput, JSONDict, ODVInput, DVInput
+
+if TYPE_CHECKING:
+ from telegram.ext import Defaults
+ from telegram import (
+ InputMediaAudio,
+ InputMediaDocument,
+ InputMediaPhoto,
+ InputMediaVideo,
+ InputMedia,
+ InlineQueryResult,
+ LabeledPrice,
+ MessageEntity,
+ )
+
+RT = TypeVar('RT')
+
+
+def log( # skipcq: PY-D0003
+ func: Callable[..., RT], *args: object, **kwargs: object # pylint: disable=W0613
+) -> Callable[..., RT]:
logger = logging.getLogger(func.__module__)
@functools.wraps(func)
- def decorator(self, *args, **kwargs):
+ def decorator(*args: object, **kwargs: object) -> RT: # pylint: disable=W0613
logger.debug('Entering: %s', func.__name__)
- result = func(self, *args, **kwargs)
+ result = func(*args, **kwargs)
logger.debug(result)
logger.debug('Exiting: %s', func.__name__)
return result
@@ -80,36 +135,19 @@ def decorator(self, *args, **kwargs):
return decorator
-def message(func):
- @functools.wraps(func)
- def decorator(self, *args, **kwargs):
- url, data = func(self, *args, **kwargs)
- if kwargs.get('reply_to_message_id'):
- data['reply_to_message_id'] = kwargs.get('reply_to_message_id')
-
- if kwargs.get('disable_notification'):
- data['disable_notification'] = kwargs.get('disable_notification')
-
- if kwargs.get('reply_markup'):
- reply_markup = kwargs.get('reply_markup')
- if isinstance(reply_markup, ReplyMarkup):
- data['reply_markup'] = reply_markup.to_json()
- else:
- data['reply_markup'] = reply_markup
-
- result = self._request.post(url, data, timeout=kwargs.get('timeout'))
-
- if result is True:
- return result
-
- return Message.de_json(result, self)
-
- return decorator
-
-
class Bot(TelegramObject):
"""This object represents a Telegram Bot.
+ .. versionadded:: 13.2
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`bot` is equal.
+
+ Note:
+ Most bot methods have the argument ``api_kwargs`` which allows to pass arbitrary keywords
+ to the Telegram API. This can be used to access new features of the API before they were
+ incorporated into PTB. However, this is not guaranteed to work, i.e. it will fail for
+ passing files.
+
Args:
token (:obj:`str`): Bot's unique authentication.
base_url (:obj:`str`, optional): Telegram Bot API service URL.
@@ -118,13 +156,50 @@ class Bot(TelegramObject):
:obj:`telegram.utils.request.Request`.
private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data.
private_key_password (:obj:`bytes`, optional): Password for above private key.
+ defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to
+ be used if not set explicitly in the bot methods.
+
+ .. deprecated:: 13.6
+ Passing :class:`telegram.ext.Defaults` to :class:`telegram.Bot` is deprecated. If
+ you want to use :class:`telegram.ext.Defaults`, please use
+ :class:`telegram.ext.ExtBot` instead.
"""
- def __init__(self, token, base_url=None, base_file_url=None, request=None, private_key=None,
- private_key_password=None):
+ __slots__ = (
+ 'token',
+ 'base_url',
+ 'base_file_url',
+ 'private_key',
+ 'defaults',
+ '_bot',
+ '_commands',
+ '_request',
+ 'logger',
+ )
+
+ def __init__(
+ self,
+ token: str,
+ base_url: str = None,
+ base_file_url: str = None,
+ request: 'Request' = None,
+ private_key: bytes = None,
+ private_key_password: bytes = None,
+ defaults: 'Defaults' = None,
+ ):
self.token = self._validate_token(token)
+ # Gather default
+ self.defaults = defaults
+
+ if self.defaults:
+ warnings.warn(
+ 'Passing Defaults to telegram.Bot is deprecated. Use telegram.ext.ExtBot instead.',
+ TelegramDeprecationWarning,
+ stacklevel=3,
+ )
+
if base_url is None:
base_url = 'https://api.telegram.org/bot'
@@ -133,21 +208,144 @@ def __init__(self, token, base_url=None, base_file_url=None, request=None, priva
self.base_url = str(base_url) + str(self.token)
self.base_file_url = str(base_file_url) + str(self.token)
- self.bot = None
+ self._bot: Optional[User] = None
+ self._commands: Optional[List[BotCommand]] = None
self._request = request or Request()
+ self.private_key = None
self.logger = logging.getLogger(__name__)
if private_key:
- self.private_key = serialization.load_pem_private_key(private_key,
- password=private_key_password,
- backend=default_backend())
+ if not CRYPTO_INSTALLED:
+ raise RuntimeError(
+ 'To use Telegram Passports, PTB must be installed via `pip install '
+ 'python-telegram-bot[passport]`.'
+ )
+ self.private_key = serialization.load_pem_private_key(
+ private_key, password=private_key_password, backend=default_backend()
+ )
+
+ # The ext_bot argument is a little hack to get warnings handled correctly.
+ # It's not very clean, but the warnings will be dropped at some point anyway.
+ def __setattr__(self, key: str, value: object, ext_bot: bool = False) -> None:
+ if issubclass(self.__class__, Bot) and self.__class__ is not Bot and not ext_bot:
+ object.__setattr__(self, key, value)
+ return
+ super().__setattr__(key, value)
+
+ def _insert_defaults(
+ self, data: Dict[str, object], timeout: ODVInput[float]
+ ) -> Optional[float]:
+ """
+ Inserts the defaults values for optional kwargs for which tg.ext.Defaults provides
+ convenience functionality, i.e. the kwargs with a tg.utils.helpers.DefaultValue default
+
+ data is edited in-place. As timeout is not passed via the kwargs, it needs to be passed
+ separately and gets returned.
+
+ This can only work, if all kwargs that may have defaults are passed in data!
+ """
+ effective_timeout = DefaultValue.get_value(timeout)
+
+ # If we have no Defaults, we just need to replace DefaultValue instances
+ # with the actual value
+ if not self.defaults:
+ data.update((key, DefaultValue.get_value(value)) for key, value in data.items())
+ return effective_timeout
+
+ # if we have Defaults, we replace all DefaultValue instances with the relevant
+ # Defaults value. If there is none, we fall back to the default value of the bot method
+ for key, val in data.items():
+ if isinstance(val, DefaultValue):
+ data[key] = self.defaults.api_defaults.get(key, val.value)
+
+ if isinstance(timeout, DefaultValue):
+ # If we get here, we use Defaults.timeout, unless that's not set, which is the
+ # case if isinstance(self.defaults.timeout, DefaultValue)
+ return (
+ self.defaults.timeout
+ if not isinstance(self.defaults.timeout, DefaultValue)
+ else effective_timeout
+ )
+ return effective_timeout
+
+ def _post(
+ self,
+ endpoint: str,
+ data: JSONDict = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union[bool, JSONDict, None]:
+ if data is None:
+ data = {}
+
+ if api_kwargs:
+ if data:
+ data.update(api_kwargs)
+ else:
+ data = api_kwargs
+
+ # Insert is in-place, so no return value for data
+ if endpoint != 'getUpdates':
+ effective_timeout = self._insert_defaults(data, timeout)
+ else:
+ effective_timeout = cast(float, timeout)
+ # Drop any None values because Telegram doesn't handle them well
+ data = {key: value for key, value in data.items() if value is not None}
+
+ return self.request.post(
+ f'{self.base_url}/{endpoint}', data=data, timeout=effective_timeout
+ )
+
+ def _message(
+ self,
+ endpoint: str,
+ data: JSONDict,
+ reply_to_message_id: int = None,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_markup: ReplyMarkup = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> Union[bool, Message]:
+ if reply_to_message_id is not None:
+ data['reply_to_message_id'] = reply_to_message_id
+
+ if protect_content:
+ data['protect_content'] = protect_content
+
+ # We don't check if (DEFAULT_)None here, so that _put is able to insert the defaults
+ # correctly, if necessary
+ data['disable_notification'] = disable_notification
+ data['allow_sending_without_reply'] = allow_sending_without_reply
+
+ if reply_markup is not None:
+ if isinstance(reply_markup, ReplyMarkup):
+ # We need to_json() instead of to_dict() here, because reply_markups may be
+ # attached to media messages, which aren't json dumped by utils.request
+ data['reply_markup'] = reply_markup.to_json()
+ else:
+ data['reply_markup'] = reply_markup
+
+ if data.get('media') and (data['media'].parse_mode == DEFAULT_NONE):
+ if self.defaults:
+ data['media'].parse_mode = DefaultValue.get_value(self.defaults.parse_mode)
+ else:
+ data['media'].parse_mode = None
+
+ result = self._post(endpoint, data, timeout=timeout, api_kwargs=api_kwargs)
+
+ if result is True:
+ return result
+
+ return Message.de_json(result, self) # type: ignore[return-value, arg-type]
@property
- def request(self):
+ def request(self) -> Request: # skip-cq: PY-D0003
return self._request
@staticmethod
- def _validate_token(token):
+ def _validate_token(token: str) -> str:
"""A very basic validation on token."""
if any(x.isspace() for x in token):
raise InvalidToken()
@@ -159,189 +357,274 @@ def _validate_token(token):
return token
@property
- @info
- def id(self):
- """:obj:`int`: Unique identifier for this bot."""
+ def bot(self) -> User:
+ """:class:`telegram.User`: User instance for the bot as returned by :meth:`get_me`."""
+ if self._bot is None:
+ self._bot = self.get_me()
+ return self._bot
+ @property
+ def id(self) -> int: # pylint: disable=C0103
+ """:obj:`int`: Unique identifier for this bot."""
return self.bot.id
@property
- @info
- def first_name(self):
+ def first_name(self) -> str:
""":obj:`str`: Bot's first name."""
-
return self.bot.first_name
@property
- @info
- def last_name(self):
+ def last_name(self) -> str:
""":obj:`str`: Optional. Bot's last name."""
-
- return self.bot.last_name
+ return self.bot.last_name # type: ignore
@property
- @info
- def username(self):
+ def username(self) -> str:
""":obj:`str`: Bot's username."""
+ return self.bot.username # type: ignore
- return self.bot.username
+ @property
+ def link(self) -> str:
+ """:obj:`str`: Convenience property. Returns the t.me link of the bot."""
+ return f"https://t.me/{self.username}"
@property
- def name(self):
- """:obj:`str`: Bot's @username."""
+ def can_join_groups(self) -> bool:
+ """:obj:`bool`: Bot's :attr:`telegram.User.can_join_groups` attribute."""
+ return self.bot.can_join_groups # type: ignore
+
+ @property
+ def can_read_all_group_messages(self) -> bool:
+ """:obj:`bool`: Bot's :attr:`telegram.User.can_read_all_group_messages` attribute."""
+ return self.bot.can_read_all_group_messages # type: ignore
+
+ @property
+ def supports_inline_queries(self) -> bool:
+ """:obj:`bool`: Bot's :attr:`telegram.User.supports_inline_queries` attribute."""
+ return self.bot.supports_inline_queries # type: ignore
- return '@{0}'.format(self.username)
+ @property
+ def commands(self) -> List[BotCommand]:
+ """
+ List[:class:`BotCommand`]: Bot's commands as available in the default scope.
+
+ .. deprecated:: 13.7
+ This property has been deprecated since there can be different commands available for
+ different scopes.
+ """
+ warnings.warn(
+ "Bot.commands has been deprecated since there can be different command "
+ "lists for different scopes.",
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+
+ if self._commands is None:
+ self._commands = self.get_my_commands()
+ return self._commands
+
+ @property
+ def name(self) -> str:
+ """:obj:`str`: Bot's @username."""
+ return f'@{self.username}'
@log
- def get_me(self, timeout=None, **kwargs):
+ def get_me(self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None) -> User:
"""A simple method for testing your bot's auth token. Requires no parameters.
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.User`: A :class:`telegram.User` instance representing that bot if the
credentials are valid, :obj:`None` otherwise.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/getMe'.format(self.base_url)
+ result = self._post('getMe', timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.get(url, timeout=timeout)
+ self._bot = User.de_json(result, self) # type: ignore[return-value, arg-type]
- self.bot = User.de_json(result, self)
-
- return self.bot
+ return self._bot # type: ignore[return-value]
@log
- @message
- def send_message(self,
- chat_id,
- text,
- parse_mode=None,
- disable_web_page_preview=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=None,
- **kwargs):
+ def send_message(
+ self,
+ chat_id: Union[int, str],
+ text: str,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ protect_content: bool = None,
+ ) -> Message:
"""Use this method to send text messages.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- text (:obj:`str`): Text of the message to be sent. Max 4096 characters. Also found as
- :attr:`telegram.constants.MAX_MESSAGE_LENGTH`.
+ of the target channel (in the format ``@channelusername``).
+ text (:obj:`str`): Text of the message to be sent. Max 4096 characters after entities
+ parsing. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`.
parse_mode (:obj:`str`): Send Markdown or HTML, if you want Telegram apps to show bold,
italic, fixed-width text or inline URLs in your bot's message. See the constants in
:class:`telegram.ParseMode` for the available modes.
+ entities (List[:class:`telegram.MessageEntity`], optional): List of special entities
+ that appear in message text, which can be specified instead of :attr:`parse_mode`.
disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in
this message.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of sent messages from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options.
A JSON-serialized object for an inline keyboard, custom reply keyboard,
instructions to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendMessage'.format(self.base_url)
-
- data = {'chat_id': chat_id, 'text': text}
-
- if parse_mode:
- data['parse_mode'] = parse_mode
- if disable_web_page_preview:
- data['disable_web_page_preview'] = disable_web_page_preview
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'text': text,
+ 'parse_mode': parse_mode,
+ 'disable_web_page_preview': disable_web_page_preview,
+ }
- return url, data
+ if entities:
+ data['entities'] = [me.to_dict() for me in entities]
+
+ return self._message( # type: ignore[return-value]
+ 'sendMessage',
+ data,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- def delete_message(self, chat_id, message_id, timeout=None, **kwargs):
+ def delete_message(
+ self,
+ chat_id: Union[str, int],
+ message_id: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""
- Use this method to delete a message. A message can only be deleted if it was sent less
- than 48 hours ago. Any such recently sent outgoing message may be deleted. Additionally,
- if the bot is an administrator in a group chat, it can delete any message. If the bot is
- an administrator in a supergroup, it can delete messages from any other user and service
- messages about people joining or leaving the group (other types of service messages may
- only be removed by the group creator). In channels, bots can only remove their own
- messages.
+ Use this method to delete a message, including service messages, with the following
+ limitations:
+
+ - A message can only be deleted if it was sent less than 48 hours ago.
+ - A dice message in a private chat can only be deleted if it was sent more than 24
+ hours ago.
+ - Bots can delete outgoing messages in private chats, groups, and supergroups.
+ - Bots can delete incoming messages in private chats.
+ - Bots granted :attr:`telegram.ChatMember.can_post_messages` permissions can delete
+ outgoing messages in channels.
+ - If the bot is an administrator of a group, it can delete any message there.
+ - If the bot has :attr:`telegram.ChatMember.can_delete_messages` permission in a
+ supergroup or a channel, it can delete any message there.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ of the target channel (in the format ``@channelusername``).
message_id (:obj:`int`): Identifier of the message to delete.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout
- from the server (instead of the one specified during creation of the connection
- pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/deleteMessage'.format(self.base_url)
+ data: JSONDict = {'chat_id': chat_id, 'message_id': message_id}
- data = {'chat_id': chat_id, 'message_id': message_id}
+ result = self._post('deleteMessage', data, timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.post(url, data, timeout=timeout)
-
- return result
+ return result # type: ignore[return-value]
@log
- @message
- def forward_message(self,
- chat_id,
- from_chat_id,
- message_id,
- disable_notification=False,
- timeout=None,
- **kwargs):
- """Use this method to forward messages of any kind.
+ def forward_message(
+ self,
+ chat_id: Union[int, str],
+ from_chat_id: Union[str, int],
+ message_id: int,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> Message:
+ """Use this method to forward messages of any kind. Service messages can't be forwarded.
+
+ Note:
+ Since the release of Bot API 5.5 it can be impossible to forward messages from
+ some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and
+ :attr:`telegram.Chat.has_protected_content` to check this.
+
+ As a workaround, it is still possible to use :meth:`copy_message`. However, this
+ behaviour is undocumented and might be changed by Telegram.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ of the target channel (in the format ``@channelusername``).
from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the
- original message was sent (or channel username in the format @channelusername).
+ original message was sent (or channel username in the format ``@channelusername``).
+ message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
- message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout
- from the server (instead of the one specified during creation of the connection
- pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/forwardMessage'.format(self.base_url)
-
- data = {}
+ data: JSONDict = {}
if chat_id:
data['chat_id'] = chat_id
@@ -349,21 +632,32 @@ def forward_message(self,
data['from_chat_id'] = from_chat_id
if message_id:
data['message_id'] = message_id
-
- return url, data
+ return self._message( # type: ignore[return-value]
+ 'forwardMessage',
+ data,
+ disable_notification=disable_notification,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- @message
- def send_photo(self,
- chat_id,
- photo,
- caption=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=20,
- parse_mode=None,
- **kwargs):
+ def send_photo(
+ self,
+ chat_id: Union[int, str],
+ photo: Union[FileInput, 'PhotoSize'],
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> Message:
"""Use this method to send photos.
Note:
@@ -372,73 +666,107 @@ def send_photo(self,
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- photo (:obj:`str` | `filelike object` | :class:`telegram.PhotoSize`): Photo to send.
+ of the target channel (in the format ``@channelusername``).
+ photo (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.PhotoSize`): Photo to send.
Pass a file_id as String to send a photo that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get a photo from the
Internet, or upload a new photo using multipart/form-data. Lastly you can pass
an existing :class:`telegram.PhotoSize` object to send.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the photo, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
caption (:obj:`str`, optional): Photo caption (may also be used when resending photos
- by file_id), 0-200 characters.
+ by file_id), 0-1024 characters after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in message text, which can be specified instead of
+ :attr:`parse_mode`.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendPhoto'.format(self.base_url)
-
- if isinstance(photo, PhotoSize):
- photo = photo.file_id
- elif InputFile.is_file(photo):
- photo = InputFile(photo)
-
- data = {'chat_id': chat_id, 'photo': photo}
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'photo': parse_file_input(photo, PhotoSize, filename=filename),
+ 'parse_mode': parse_mode,
+ }
if caption:
data['caption'] = caption
- if parse_mode:
- data['parse_mode'] = parse_mode
-
- return url, data
-
- @log
- @message
- def send_audio(self,
- chat_id,
- audio,
- duration=None,
- performer=None,
- title=None,
- caption=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=20,
- parse_mode=None,
- thumb=None,
- **kwargs):
+
+ if caption_entities:
+ data['caption_entities'] = [me.to_dict() for me in caption_entities]
+
+ return self._message( # type: ignore[return-value]
+ 'sendPhoto',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ @log
+ def send_audio(
+ self,
+ chat_id: Union[int, str],
+ audio: Union[FileInput, 'Audio'],
+ duration: int = None,
+ performer: str = None,
+ title: str = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> Message:
"""
Use this method to send audio files, if you want Telegram clients to display them in the
- music player. Your audio must be in the .mp3 format. On success, the sent Message is
- returned. Bots can currently send audio files of up to 50 MB in size, this limit may be
- changed in the future.
+ music player. Your audio must be in the .mp3 or .m4a format.
- For sending voice messages, use the sendVoice method instead.
+ Bots can currently send audio files of up to 50 MB in size, this limit may be changed in
+ the future.
+
+ For sending voice messages, use the :meth:`send_voice` method instead.
Note:
The audio argument can be either a file_id, an URL or a file from disk
@@ -446,48 +774,71 @@ def send_audio(self,
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- audio (:obj:`str` | `filelike object` | :class:`telegram.Audio`): Audio file to send.
+ of the target channel (in the format ``@channelusername``).
+ audio (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Audio`): Audio file to send.
Pass a file_id as String to send an audio file that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get an audio file from
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Audio` object to send.
- caption (:obj:`str`, optional): Audio caption, 0-200 characters.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the audio, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
+ caption (:obj:`str`, optional): Audio caption, 0-1024 characters after entities
+ parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in message text, which can be specified instead of
+ :attr:`parse_mode`.
duration (:obj:`int`, optional): Duration of sent audio in seconds.
performer (:obj:`str`, optional): Performer.
title (:obj:`str`, optional): Track name.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
- thumb (`filelike object`, optional): Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail
+ of the file sent; can be ignored if
+ thumbnail generation for the file is supported server-side. The thumbnail should be
+ in JPEG format and less than 200 kB in size. A thumbnail's width and height should
+ not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
+ Thumbnails can't be reused and can be only uploaded as a new file.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendAudio'.format(self.base_url)
-
- if isinstance(audio, Audio):
- audio = audio.file_id
- elif InputFile.is_file(audio):
- audio = InputFile(audio)
-
- data = {'chat_id': chat_id, 'audio': audio}
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'audio': parse_file_input(audio, Audio, filename=filename),
+ 'parse_mode': parse_mode,
+ }
if duration:
data['duration'] = duration
@@ -497,30 +848,48 @@ def send_audio(self,
data['title'] = title
if caption:
data['caption'] = caption
- if parse_mode:
- data['parse_mode'] = parse_mode
+
+ if caption_entities:
+ data['caption_entities'] = [me.to_dict() for me in caption_entities]
if thumb:
- if InputFile.is_file(thumb):
- thumb = InputFile(thumb, attach=True)
- data['thumb'] = thumb
-
- return url, data
-
- @log
- @message
- def send_document(self,
- chat_id,
- document,
- filename=None,
- caption=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=20,
- parse_mode=None,
- thumb=None,
- **kwargs):
- """Use this method to send general files.
+ data['thumb'] = parse_file_input(thumb, attach=True)
+
+ return self._message( # type: ignore[return-value]
+ 'sendAudio',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ @log
+ def send_document(
+ self,
+ chat_id: Union[int, str],
+ document: Union[FileInput, 'Document'],
+ filename: str = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ disable_content_type_detection: bool = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ protect_content: bool = None,
+ ) -> Message:
+ """
+ Use this method to send general files.
+
+ Bots can currently send files of any type of up to 50 MB in size, this limit may be
+ changed in the future.
Note:
The document argument can be either a file_id, an URL or a file from disk
@@ -528,71 +897,106 @@ def send_document(self,
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- document (:obj:`str` | `filelike object` | :class:`telegram.Document`): File to send.
+ of the target channel (in the format ``@channelusername``).
+ document (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Document`): File to send.
Pass a file_id as String to send a file that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get a file from the
Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Document` object to send.
- filename (:obj:`str`, optional): File name that shows in telegram message (it is useful
- when you send file generated by temp module, for example). Undocumented.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the document, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
caption (:obj:`str`, optional): Document caption (may also be used when resending
- documents by file_id), 0-200 characters.
+ documents by file_id), 0-1024 characters after entities parsing.
+ disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side
+ content type detection for files uploaded using multipart/form-data.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in message text, which can be specified instead of
+ :attr:`parse_mode`.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
- thumb (`filelike object`, optional): Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail
+ of the file sent; can be ignored if
+ thumbnail generation for the file is supported server-side. The thumbnail should be
+ in JPEG format and less than 200 kB in size. A thumbnail's width and height should
+ not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
+ Thumbnails can't be reused and can be only uploaded as a new file.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendDocument'.format(self.base_url)
-
- if isinstance(document, Document):
- document = document.file_id
- elif InputFile.is_file(document):
- document = InputFile(document, filename=filename)
-
- data = {'chat_id': chat_id, 'document': document}
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'document': parse_file_input(document, Document, filename=filename),
+ 'parse_mode': parse_mode,
+ }
if caption:
data['caption'] = caption
- if parse_mode:
- data['parse_mode'] = parse_mode
- if thumb:
- if InputFile.is_file(thumb):
- thumb = InputFile(thumb, attach=True)
- data['thumb'] = thumb
- return url, data
+ if caption_entities:
+ data['caption_entities'] = [me.to_dict() for me in caption_entities]
+ if disable_content_type_detection is not None:
+ data['disable_content_type_detection'] = disable_content_type_detection
+ if thumb:
+ data['thumb'] = parse_file_input(thumb, attach=True)
+
+ return self._message( # type: ignore[return-value]
+ 'sendDocument',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- @message
- def send_sticker(self,
- chat_id,
- sticker,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=20,
- **kwargs):
- """Use this method to send .webp stickers.
+ def send_sticker(
+ self,
+ chat_id: Union[int, str],
+ sticker: Union[FileInput, 'Sticker'],
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> Message:
+ """
+ Use this method to send static ``.WEBP``, animated ``.TGS``, or video ``.WEBM`` stickers.
Note:
The sticker argument can be either a file_id, an URL or a file from disk
@@ -600,119 +1004,167 @@ def send_sticker(self,
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- sticker (:obj:`str` | `filelike object` :class:`telegram.Sticker`): Sticker to send.
+ of the target channel (in the format ``@channelusername``).
+ sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Sticker`): Sticker to send.
Pass a file_id as String to send a file that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get a .webp file from
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Sticker` object to send.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendSticker'.format(self.base_url)
-
- if isinstance(sticker, Sticker):
- sticker = sticker.file_id
- elif InputFile.is_file(sticker):
- sticker = InputFile(sticker)
-
- data = {'chat_id': chat_id, 'sticker': sticker}
-
- return url, data
+ data: JSONDict = {'chat_id': chat_id, 'sticker': parse_file_input(sticker, Sticker)}
+
+ return self._message( # type: ignore[return-value]
+ 'sendSticker',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- @message
- def send_video(self,
- chat_id,
- video,
- duration=None,
- caption=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=20,
- width=None,
- height=None,
- parse_mode=None,
- supports_streaming=None,
- thumb=None,
- **kwargs):
+ def send_video(
+ self,
+ chat_id: Union[int, str],
+ video: Union[FileInput, 'Video'],
+ duration: int = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ width: int = None,
+ height: int = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ supports_streaming: bool = None,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> Message:
"""
Use this method to send video files, Telegram clients support mp4 videos
(other formats may be sent as Document).
+ Bots can currently send video files of up to 50 MB in size, this limit may be changed in
+ the future.
+
Note:
- The video argument can be either a file_id, an URL or a file from disk
- ``open(filename, 'rb')``
+ * The video argument can be either a file_id, an URL or a file from disk
+ ``open(filename, 'rb')``
+ * ``thumb`` will be ignored for small video files, for which Telegram can easily
+ generate thumb nails. However, this behaviour is undocumented and might be changed
+ by Telegram.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- video (:obj:`str` | `filelike object` | :class:`telegram.Video`): Video file to send.
+ of the target channel (in the format ``@channelusername``).
+ video (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Video`): Video file to send.
Pass a file_id as String to send an video file that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get an video file from
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Video` object to send.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the video, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
duration (:obj:`int`, optional): Duration of sent video in seconds.
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
caption (:obj:`str`, optional): Video caption (may also be used when resending videos
- by file_id), 0-200 characters.
+ by file_id), 0-1024 characters after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
- supports_streaming (:obj:`bool`, optional): Pass True, if the uploaded video is
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in message text, which can be specified instead of
+ :attr:`parse_mode`.
+ supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is
suitable for streaming.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
- thumb (`filelike object`, optional): Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail
+ of the file sent; can be ignored if
+ thumbnail generation for the file is supported server-side. The thumbnail should be
+ in JPEG format and less than 200 kB in size. A thumbnail's width and height should
+ not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
+ Thumbnails can't be reused and can be only uploaded as a new file.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendVideo'.format(self.base_url)
-
- if isinstance(video, Video):
- video = video.file_id
- elif InputFile.is_file(video):
- video = InputFile(video)
-
- data = {'chat_id': chat_id, 'video': video}
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'video': parse_file_input(video, Video, filename=filename),
+ 'parse_mode': parse_mode,
+ }
if duration:
data['duration'] = duration
if caption:
data['caption'] = caption
- if parse_mode:
- data['parse_mode'] = parse_mode
+ if caption_entities:
+ data['caption_entities'] = [me.to_dict() for me in caption_entities]
if supports_streaming:
data['supports_streaming'] = supports_streaming
if width:
@@ -720,146 +1172,224 @@ def send_video(self,
if height:
data['height'] = height
if thumb:
- if InputFile.is_file(thumb):
- thumb = InputFile(thumb, attach=True)
- data['thumb'] = thumb
-
- return url, data
-
- @log
- @message
- def send_video_note(self,
- chat_id,
- video_note,
- duration=None,
- length=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=20,
- thumb=None,
- **kwargs):
- """Use this method to send video messages.
+ data['thumb'] = parse_file_input(thumb, attach=True)
+
+ return self._message( # type: ignore[return-value]
+ 'sendVideo',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ @log
+ def send_video_note(
+ self,
+ chat_id: Union[int, str],
+ video_note: Union[FileInput, 'VideoNote'],
+ duration: int = None,
+ length: int = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> Message:
+ """
+ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long.
+ Use this method to send video messages.
Note:
- The video_note argument can be either a file_id or a file from disk
- ``open(filename, 'rb')``
+ * The video_note argument can be either a file_id or a file from disk
+ ``open(filename, 'rb')``
+ * ``thumb`` will be ignored for small video files, for which Telegram can easily
+ generate thumb nails. However, this behaviour is undocumented and might be changed
+ by Telegram.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- video_note (:obj:`str` | `filelike object` | :class:`telegram.VideoNote`): Video note
+ of the target channel (in the format ``@channelusername``).
+ video_note (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.VideoNote`): Video note
to send. Pass a file_id as String to send a video note that exists on the Telegram
servers (recommended) or upload a new video using multipart/form-data. Or you can
pass an existing :class:`telegram.VideoNote` object to send. Sending video notes by
a URL is currently unsupported.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the video note, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
duration (:obj:`int`, optional): Duration of sent video in seconds.
- length (:obj:`int`, optional): Video width and height
+ length (:obj:`int`, optional): Video width and height, i.e. diameter of the video
+ message.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard,
instructions to remove reply keyboard or to force a reply from the user.
- thumb (`filelike object`, optional): Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail
+ of the file sent; can be ignored if
+ thumbnail generation for the file is supported server-side. The thumbnail should be
+ in JPEG format and less than 200 kB in size. A thumbnail's width and height should
+ not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
+ Thumbnails can't be reused and can be only uploaded as a new file.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendVideoNote'.format(self.base_url)
-
- if isinstance(video_note, VideoNote):
- video_note = video_note.file_id
- elif InputFile.is_file(video_note):
- video_note = InputFile(video_note)
-
- data = {'chat_id': chat_id, 'video_note': video_note}
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'video_note': parse_file_input(video_note, VideoNote, filename=filename),
+ }
if duration is not None:
data['duration'] = duration
if length is not None:
data['length'] = length
if thumb:
- if InputFile.is_file(thumb):
- thumb = InputFile(thumb, attach=True)
- data['thumb'] = thumb
-
- return url, data
-
- @log
- @message
- def send_animation(self,
- chat_id,
- animation,
- duration=None,
- width=None,
- height=None,
- thumb=None,
- caption=None,
- parse_mode=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=20,
- **kwargs):
+ data['thumb'] = parse_file_input(thumb, attach=True)
+
+ return self._message( # type: ignore[return-value]
+ 'sendVideoNote',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ @log
+ def send_animation(
+ self,
+ chat_id: Union[int, str],
+ animation: Union[FileInput, 'Animation'],
+ duration: int = None,
+ width: int = None,
+ height: int = None,
+ thumb: FileInput = None,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> Message:
"""
Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
+ Bots can currently send animation files of up to 50 MB in size, this limit may be changed
+ in the future.
+
+ Note:
+ ``thumb`` will be ignored for small files, for which Telegram can easily
+ generate thumb nails. However, this behaviour is undocumented and might be changed
+ by Telegram.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- animation (:obj:`str` | `filelike object` | :class:`telegram.Animation`): Animation to
+ of the target channel (in the format ``@channelusername``).
+ animation (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Animation`): Animation to
send. Pass a file_id as String to send an animation that exists on the Telegram
servers (recommended), pass an HTTP URL as a String for Telegram to get an
animation from the Internet, or upload a new animation using multipart/form-data.
Lastly you can pass an existing :class:`telegram.Animation` object to send.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the animation, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
duration (:obj:`int`, optional): Duration of sent animation in seconds.
width (:obj:`int`, optional): Animation width.
height (:obj:`int`, optional): Animation height.
- thumb (`filelike object`, optional): Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail
+ of the file sent; can be ignored if
+ thumbnail generation for the file is supported server-side. The thumbnail should be
+ in JPEG format and less than 200 kB in size. A thumbnail's width and height should
+ not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
+ Thumbnails can't be reused and can be only uploaded as a new file.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
caption (:obj:`str`, optional): Animation caption (may also be used when resending
- animations by file_id), 0-200 characters.
+ animations by file_id), 0-1024 characters after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in message text, which can be specified instead of
+ :attr:`parse_mode`.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendAnimation'.format(self.base_url)
-
- if isinstance(animation, Animation):
- animation = animation.file_id
- elif InputFile.is_file(animation):
- animation = InputFile(animation)
-
- data = {'chat_id': chat_id, 'animation': animation}
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'animation': parse_file_input(animation, Animation, filename=filename),
+ 'parse_mode': parse_mode,
+ }
if duration:
data['duration'] = duration
@@ -868,33 +1398,47 @@ def send_animation(self,
if height:
data['height'] = height
if thumb:
- if InputFile.is_file(thumb):
- thumb = InputFile(thumb, attach=True)
- data['thumb'] = thumb
+ data['thumb'] = parse_file_input(thumb, attach=True)
if caption:
data['caption'] = caption
- if parse_mode:
- data['parse_mode'] = parse_mode
-
- return url, data
+ if caption_entities:
+ data['caption_entities'] = [me.to_dict() for me in caption_entities]
+
+ return self._message( # type: ignore[return-value]
+ 'sendAnimation',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- @message
- def send_voice(self,
- chat_id,
- voice,
- duration=None,
- caption=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=20,
- parse_mode=None,
- **kwargs):
+ def send_voice(
+ self,
+ chat_id: Union[int, str],
+ voice: Union[FileInput, 'Voice'],
+ duration: int = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> Message:
"""
Use this method to send audio files, if you want Telegram clients to display the file
as a playable voice message. For this to work, your audio must be in an .ogg file
- encoded with OPUS (other formats may be sent as Audio or Document).
+ encoded with OPUS (other formats may be sent as Audio or Document). Bots can currently
+ send voice messages of up to 50 MB in size, this limit may be changed in the future.
Note:
The voice argument can be either a file_id, an URL or a file from disk
@@ -902,107 +1446,167 @@ def send_voice(self,
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- voice (:obj:`str` | `filelike object` | :class:`telegram.Voice`): Voice file to send.
+ of the target channel (in the format ``@channelusername``).
+ voice (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Voice`): Voice file to send.
Pass a file_id as String to send an voice file that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get an voice file from
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Voice` object to send.
- caption (:obj:`str`, optional): Voice message caption, 0-200 characters.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the voice, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
+ caption (:obj:`str`, optional): Voice message caption, 0-1024 characters after entities
+ parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in message text, which can be specified instead of
+ :attr:`parse_mode`.
duration (:obj:`int`, optional): Duration of the voice message in seconds.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard,
instructions to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendVoice'.format(self.base_url)
-
- if isinstance(voice, Voice):
- voice = voice.file_id
- elif InputFile.is_file(voice):
- voice = InputFile(voice)
-
- data = {'chat_id': chat_id, 'voice': voice}
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'voice': parse_file_input(voice, Voice, filename=filename),
+ 'parse_mode': parse_mode,
+ }
if duration:
data['duration'] = duration
if caption:
data['caption'] = caption
- if parse_mode:
- data['parse_mode'] = parse_mode
- return url, data
+ if caption_entities:
+ data['caption_entities'] = [me.to_dict() for me in caption_entities]
+
+ return self._message( # type: ignore[return-value]
+ 'sendVoice',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- def send_media_group(self,
- chat_id,
- media,
- disable_notification=None,
- reply_to_message_id=None,
- timeout=20,
- **kwargs):
+ def send_media_group(
+ self,
+ chat_id: Union[int, str],
+ media: List[
+ Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo']
+ ],
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> List[Message]:
"""Use this method to send a group of photos or videos as an album.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
- media (List[:class:`telegram.InputMedia`]): An array describing photos and videos to be
- sent, must include 2–10 items.
+ of the target channel (in the format ``@channelusername``).
+ media (List[:class:`telegram.InputMediaAudio`, :class:`telegram.InputMediaDocument`, \
+ :class:`telegram.InputMediaPhoto`, :class:`telegram.InputMediaVideo`]): An array
+ describing messages to be sent, must include 2–10 items.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
List[:class:`telegram.Message`]: An array of the sent Messages.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'media': media,
+ 'disable_notification': disable_notification,
+ 'allow_sending_without_reply': allow_sending_without_reply,
+ }
- url = '{0}/sendMediaGroup'.format(self.base_url)
-
- data = {'chat_id': chat_id, 'media': media}
+ for med in data['media']:
+ if med.parse_mode == DEFAULT_NONE:
+ if self.defaults:
+ med.parse_mode = DefaultValue.get_value(self.defaults.parse_mode)
+ else:
+ med.parse_mode = None
if reply_to_message_id:
data['reply_to_message_id'] = reply_to_message_id
- if disable_notification:
- data['disable_notification'] = disable_notification
-
- result = self._request.post(url, data, timeout=timeout)
-
- return [Message.de_json(res, self) for res in result]
-
- @log
- @message
- def send_location(self,
- chat_id,
- latitude=None,
- longitude=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=None,
- location=None,
- live_period=None,
- **kwargs):
+
+ if protect_content:
+ data['protect_content'] = protect_content
+
+ result = self._post('sendMediaGroup', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return Message.de_list(result, self) # type: ignore
+
+ @log
+ def send_location(
+ self,
+ chat_id: Union[int, str],
+ latitude: float = None,
+ longitude: float = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ location: Location = None,
+ live_period: int = None,
+ api_kwargs: JSONDict = None,
+ horizontal_accuracy: float = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> Message:
"""Use this method to send point on the map.
Note:
@@ -1010,105 +1614,150 @@ def send_location(self,
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ of the target channel (in the format ``@channelusername``).
latitude (:obj:`float`, optional): Latitude of location.
longitude (:obj:`float`, optional): Longitude of location.
location (:class:`telegram.Location`, optional): The location to send.
+ horizontal_accuracy (:obj:`int`, optional): The radius of uncertainty for the location,
+ measured in meters; 0-1500.
live_period (:obj:`int`, optional): Period in seconds for which the location will be
updated, should be between 60 and 86400.
+ heading (:obj:`int`, optional): For live locations, a direction in which the user is
+ moving, in degrees. Must be between 1 and 360 if specified.
+ proximity_alert_radius (:obj:`int`, optional): For live locations, a maximum distance
+ for proximity alerts about approaching another chat member, in meters. Must be
+ between 1 and 100000 if specified.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard,
instructions to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendLocation'.format(self.base_url)
-
- if not (all([latitude, longitude]) or location):
- raise ValueError("Either location or latitude and longitude must be passed as"
- "argument.")
+ if not ((latitude is not None and longitude is not None) or location):
+ raise ValueError(
+ "Either location or latitude and longitude must be passed as argument."
+ )
- if not ((latitude is not None or longitude is not None) ^ bool(location)):
- raise ValueError("Either location or latitude and longitude must be passed as"
- "argument. Not both.")
+ if not (latitude is not None or longitude is not None) ^ bool(location):
+ raise ValueError(
+ "Either location or latitude and longitude must be passed as argument. Not both."
+ )
if isinstance(location, Location):
latitude = location.latitude
longitude = location.longitude
- data = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude}
+ data: JSONDict = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude}
if live_period:
data['live_period'] = live_period
-
- return url, data
+ if horizontal_accuracy:
+ data['horizontal_accuracy'] = horizontal_accuracy
+ if heading:
+ data['heading'] = heading
+ if proximity_alert_radius:
+ data['proximity_alert_radius'] = proximity_alert_radius
+
+ return self._message( # type: ignore[return-value]
+ 'sendLocation',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- @message
- def edit_message_live_location(self,
- chat_id=None,
- message_id=None,
- inline_message_id=None,
- latitude=None,
- longitude=None,
- location=None,
- reply_markup=None,
- **kwargs):
+ def edit_message_live_location(
+ self,
+ chat_id: Union[str, int] = None,
+ message_id: int = None,
+ inline_message_id: int = None,
+ latitude: float = None,
+ longitude: float = None,
+ location: Location = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ horizontal_accuracy: float = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ ) -> Union[Message, bool]:
"""Use this method to edit live location messages sent by the bot or via the bot
- (for inline bots). A location can be edited until its :attr:`live_period` expires or
- editing is explicitly disabled by a call to :attr:`stop_message_live_location`.
+ (for inline bots). A location can be edited until its :attr:`telegram.Location.live_period`
+ expires or editing is explicitly disabled by a call to :meth:`stop_message_live_location`.
Note:
You can either supply a :obj:`latitude` and :obj:`longitude` or a :obj:`location`.
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
+ specified. Unique identifier for the target chat or username of the target channel
+ (in the format ``@channelusername``).
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
- Identifier of the sent message.
+ Identifier of the message to edit.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
specified. Identifier of the inline message.
latitude (:obj:`float`, optional): Latitude of location.
longitude (:obj:`float`, optional): Longitude of location.
location (:class:`telegram.Location`, optional): The location to send.
- reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
- JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
- to remove reply keyboard or to force a reply from the user.
+ horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the
+ location, measured in meters; 0-1500.
+ heading (:obj:`int`, optional): Direction in which the user is moving, in degrees. Must
+ be between 1 and 360 if specified.
+ proximity_alert_radius (:obj:`int`, optional): Maximum distance for proximity alerts
+ about approaching another chat member, in meters. Must be between 1 and 100000 if
+ specified.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized
+ object for a new inline keyboard.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :class:`telegram.Message`: On success the edited message.
+ :class:`telegram.Message`: On success, if edited message is not an inline message, the
+ edited message is returned, otherwise :obj:`True` is returned.
"""
-
- url = '{0}/editMessageLiveLocation'.format(self.base_url)
-
if not (all([latitude, longitude]) or location):
- raise ValueError("Either location or latitude and longitude must be passed as"
- "argument.")
- if not ((latitude is not None or longitude is not None) ^ bool(location)):
- raise ValueError("Either location or latitude and longitude must be passed as"
- "argument. Not both.")
+ raise ValueError(
+ "Either location or latitude and longitude must be passed as argument."
+ )
+ if not (latitude is not None or longitude is not None) ^ bool(location):
+ raise ValueError(
+ "Either location or latitude and longitude must be passed as argument. Not both."
+ )
if isinstance(location, Location):
latitude = location.latitude
longitude = location.longitude
- data = {'latitude': latitude, 'longitude': longitude}
+ data: JSONDict = {'latitude': latitude, 'longitude': longitude}
if chat_id:
data['chat_id'] = chat_id
@@ -1116,41 +1765,55 @@ def edit_message_live_location(self,
data['message_id'] = message_id
if inline_message_id:
data['inline_message_id'] = inline_message_id
-
- return url, data
+ if horizontal_accuracy:
+ data['horizontal_accuracy'] = horizontal_accuracy
+ if heading:
+ data['heading'] = heading
+ if proximity_alert_radius:
+ data['proximity_alert_radius'] = proximity_alert_radius
+
+ return self._message(
+ 'editMessageLiveLocation',
+ data,
+ timeout=timeout,
+ reply_markup=reply_markup,
+ api_kwargs=api_kwargs,
+ )
@log
- @message
- def stop_message_live_location(self,
- chat_id=None,
- message_id=None,
- inline_message_id=None,
- reply_markup=None,
- **kwargs):
+ def stop_message_live_location(
+ self,
+ chat_id: Union[str, int] = None,
+ message_id: int = None,
+ inline_message_id: int = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union[Message, bool]:
"""Use this method to stop updating a live location message sent by the bot or via the bot
(for inline bots) before live_period expires.
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ chat_id (:obj:`int` | :obj:`str`): Required if inline_message_id is not specified.
+ Unique identifier for the target chat or username of the target channel
+ (in the format ``@channelusername``).
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
- Identifier of the sent message.
+ Identifier of the sent message with live location to stop.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
specified. Identifier of the inline message.
- reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
- JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
- to remove reply keyboard or to force a reply from the user.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized
+ object for a new inline keyboard.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :class:`telegram.Message`: On success the edited message.
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ sent Message is returned, otherwise :obj:`True` is returned.
"""
-
- url = '{0}/stopMessageLiveLocation'.format(self.base_url)
-
- data = {}
+ data: JSONDict = {}
if chat_id:
data['chat_id'] = chat_id
@@ -1159,34 +1822,48 @@ def stop_message_live_location(self,
if inline_message_id:
data['inline_message_id'] = inline_message_id
- return url, data
-
- @log
- @message
- def send_venue(self,
- chat_id,
- latitude=None,
- longitude=None,
- title=None,
- address=None,
- foursquare_id=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=None,
- venue=None,
- foursquare_type=None,
- **kwargs):
+ return self._message(
+ 'stopMessageLiveLocation',
+ data,
+ timeout=timeout,
+ reply_markup=reply_markup,
+ api_kwargs=api_kwargs,
+ )
+
+ @log
+ def send_venue(
+ self,
+ chat_id: Union[int, str],
+ latitude: float = None,
+ longitude: float = None,
+ title: str = None,
+ address: str = None,
+ foursquare_id: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ venue: Venue = None,
+ foursquare_type: str = None,
+ api_kwargs: JSONDict = None,
+ google_place_id: str = None,
+ google_place_type: str = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> Message:
"""Use this method to send information about a venue.
Note:
- you can either supply :obj:`venue`, or :obj:`latitude`, :obj:`longitude`,
- :obj:`title` and :obj:`address` and optionally :obj:`foursquare_id` and optionally
- :obj:`foursquare_type`.
+ * You can either supply :obj:`venue`, or :obj:`latitude`, :obj:`longitude`,
+ :obj:`title` and :obj:`address` and optionally :obj:`foursquare_id` and
+ :obj:`foursquare_type` or optionally :obj:`google_place_id` and
+ :obj:`google_place_type`.
+ * Foursquare details and Google Pace details are mutually exclusive. However, this
+ behaviour is undocumented and might be changed by Telegram.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ of the target channel (in the format ``@channelusername``).
latitude (:obj:`float`, optional): Latitude of venue.
longitude (:obj:`float`, optional): Longitude of venue.
title (:obj:`str`, optional): Name of the venue.
@@ -1195,31 +1872,43 @@ def send_venue(self,
foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known.
(For example, "arts_entertainment/default", "arts_entertainment/aquarium" or
"food/icecream".)
+ google_place_id (:obj:`str`, optional): Google Places identifier of the venue.
+ google_place_type (:obj:`str`, optional): Google Places type of the venue. (See
+ `supported types \
+ `_.)
venue (:class:`telegram.Venue`, optional): The venue to send.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendVenue'.format(self.base_url)
-
if not (venue or all([latitude, longitude, address, title])):
- raise ValueError("Either venue or latitude, longitude, address and title must be"
- "passed as arguments.")
+ raise ValueError(
+ "Either venue or latitude, longitude, address and title must be"
+ "passed as arguments."
+ )
if isinstance(venue, Venue):
latitude = venue.location.latitude
@@ -1228,36 +1917,55 @@ def send_venue(self,
title = venue.title
foursquare_id = venue.foursquare_id
foursquare_type = venue.foursquare_type
+ google_place_id = venue.google_place_id
+ google_place_type = venue.google_place_type
- data = {
+ data: JSONDict = {
'chat_id': chat_id,
'latitude': latitude,
'longitude': longitude,
'address': address,
- 'title': title
+ 'title': title,
}
if foursquare_id:
data['foursquare_id'] = foursquare_id
if foursquare_type:
data['foursquare_type'] = foursquare_type
+ if google_place_id:
+ data['google_place_id'] = google_place_id
+ if google_place_type:
+ data['google_place_type'] = google_place_type
+
+ return self._message( # type: ignore[return-value]
+ 'sendVenue',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
- return url, data
-
- @log
- @message
- def send_contact(self,
- chat_id,
- phone_number=None,
- first_name=None,
- last_name=None,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=None,
- contact=None,
- vcard=None,
- **kwargs):
+ @log
+ def send_contact(
+ self,
+ chat_id: Union[int, str],
+ phone_number: str = None,
+ first_name: str = None,
+ last_name: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ contact: Contact = None,
+ vcard: str = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> Message:
"""Use this method to send phone contacts.
Note:
@@ -1266,7 +1974,7 @@ def send_contact(self,
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ of the target channel (in the format ``@channelusername``).
phone_number (:obj:`str`, optional): Contact's phone number.
first_name (:obj:`str`, optional): Contact's first name.
last_name (:obj:`str`, optional): Contact's last name.
@@ -1275,28 +1983,35 @@ def send_contact(self,
contact (:class:`telegram.Contact`, optional): The contact to send.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendContact'.format(self.base_url)
-
if (not contact) and (not all([phone_number, first_name])):
- raise ValueError("Either contact or phone_number and first_name must be passed as"
- "arguments.")
+ raise ValueError(
+ "Either contact or phone_number and first_name must be passed as arguments."
+ )
if isinstance(contact, Contact):
phone_number = contact.phone_number
@@ -1304,129 +2019,240 @@ def send_contact(self,
last_name = contact.last_name
vcard = contact.vcard
- data = {'chat_id': chat_id, 'phone_number': phone_number, 'first_name': first_name}
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'phone_number': phone_number,
+ 'first_name': first_name,
+ }
if last_name:
data['last_name'] = last_name
if vcard:
data['vcard'] = vcard
- return url, data
+ return self._message( # type: ignore[return-value]
+ 'sendContact',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- @message
- def send_game(self,
- chat_id,
- game_short_name,
- disable_notification=False,
- reply_to_message_id=None,
- reply_markup=None,
- timeout=None,
- **kwargs):
+ def send_game(
+ self,
+ chat_id: Union[int, str],
+ game_short_name: str,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> Message:
"""Use this method to send a game.
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat.
game_short_name (:obj:`str`): Short name of the game, serves as the unique identifier
- for the game. Set up your games via Botfather.
+ for the game. Set up your games via `@BotFather `_.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
- reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
- JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
- to remove reply keyboard or to force a reply from the user.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized
+ object for a new inline keyboard. If empty, one ‘Play game_title’ button will be
+ shown. If not empty, the first button must launch the game.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendGame'.format(self.base_url)
-
- data = {'chat_id': chat_id, 'game_short_name': game_short_name}
-
- return url, data
+ data: JSONDict = {'chat_id': chat_id, 'game_short_name': game_short_name}
+
+ return self._message( # type: ignore[return-value]
+ 'sendGame',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- def send_chat_action(self, chat_id, action, timeout=None, **kwargs):
+ def send_chat_action(
+ self,
+ chat_id: Union[str, int],
+ action: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""
Use this method when you need to tell the user that something is happening on the bot's
side. The status is set for 5 seconds or less (when a message arrives from your bot,
- Telegram clients clear its typing status).
+ Telegram clients clear its typing status). Telegram only recommends using this method when
+ a response from the bot will take a noticeable amount of time to arrive.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ of the target channel (in the format ``@channelusername``).
action(:class:`telegram.ChatAction` | :obj:`str`): Type of action to broadcast. Choose
one, depending on what the user is about to receive. For convenience look at the
constants in :class:`telegram.ChatAction`
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: ``True`` on success.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendChatAction'.format(self.base_url)
+ data: JSONDict = {'chat_id': chat_id, 'action': action}
- data = {'chat_id': chat_id, 'action': action}
- data.update(kwargs)
+ result = self._post('sendChatAction', data, timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.post(url, data, timeout=timeout)
+ return result # type: ignore[return-value]
- return result
+ def _effective_inline_results( # pylint: disable=R0201
+ self,
+ results: Union[
+ Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]]
+ ],
+ next_offset: str = None,
+ current_offset: str = None,
+ ) -> Tuple[Sequence['InlineQueryResult'], Optional[str]]:
+ """
+ Builds the effective results from the results input.
+ We make this a stand-alone method so tg.ext.ExtBot can wrap it.
+
+ Returns:
+ Tuple of 1. the effective results and 2. correct the next_offset
- @log
- def answer_inline_query(self,
- inline_query_id,
- results,
- cache_time=300,
- is_personal=None,
- next_offset=None,
- switch_pm_text=None,
- switch_pm_parameter=None,
- timeout=None,
- **kwargs):
"""
- Use this method to send answers to an inline query. No more than 50 results per query are
- allowed.
+ if current_offset is not None and next_offset is not None:
+ raise ValueError('`current_offset` and `next_offset` are mutually exclusive!')
- Args:
- inline_query_id (:obj:`str`): Unique identifier for the answered query.
- results (List[:class:`telegram.InlineQueryResult`)]: A list of results for the inline
- query.
+ if current_offset is not None:
+ # Convert the string input to integer
+ if current_offset == '':
+ current_offset_int = 0
+ else:
+ current_offset_int = int(current_offset)
+
+ # for now set to empty string, stating that there are no more results
+ # might change later
+ next_offset = ''
+
+ if callable(results):
+ callable_output = results(current_offset_int)
+ if not callable_output:
+ effective_results: Sequence['InlineQueryResult'] = []
+ else:
+ effective_results = callable_output
+ # the callback *might* return more results on the next call, so we increment
+ # the page count
+ next_offset = str(current_offset_int + 1)
+ else:
+ if len(results) > (current_offset_int + 1) * MAX_INLINE_QUERY_RESULTS:
+ # we expect more results for the next page
+ next_offset_int = current_offset_int + 1
+ next_offset = str(next_offset_int)
+ effective_results = results[
+ current_offset_int
+ * MAX_INLINE_QUERY_RESULTS : next_offset_int
+ * MAX_INLINE_QUERY_RESULTS
+ ]
+ else:
+ effective_results = results[current_offset_int * MAX_INLINE_QUERY_RESULTS :]
+ else:
+ effective_results = results # type: ignore[assignment]
+
+ return effective_results, next_offset
+
+ @log
+ def answer_inline_query(
+ self,
+ inline_query_id: str,
+ results: Union[
+ Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]]
+ ],
+ cache_time: int = 300,
+ is_personal: bool = None,
+ next_offset: str = None,
+ switch_pm_text: str = None,
+ switch_pm_parameter: str = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ current_offset: str = None,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to send answers to an inline query. No more than 50 results per query are
+ allowed.
+
+ Warning:
+ In most use cases :attr:`current_offset` should not be passed manually. Instead of
+ calling this method directly, use the shortcut :meth:`telegram.InlineQuery.answer` with
+ ``auto_pagination=True``, which will take care of passing the correct value.
+
+ Args:
+ inline_query_id (:obj:`str`): Unique identifier for the answered query.
+ results (List[:class:`telegram.InlineQueryResult`] | Callable): A list of results for
+ the inline query. In case :attr:`current_offset` is passed, ``results`` may also be
+ a callable that accepts the current page index starting from 0. It must return
+ either a list of :class:`telegram.InlineQueryResult` instances or :obj:`None` if
+ there are no more results.
cache_time (:obj:`int`, optional): The maximum amount of time in seconds that the
- result of the inline query may be cached on the server. Defaults to 300.
- is_personal (:obj:`bool`, optional): Pass True, if results may be cached on the server
- side only for the user that sent the query. By default, results may be returned to
- any user who sends the same query.
+ result of the inline query may be cached on the server. Defaults to ``300``.
+ is_personal (:obj:`bool`, optional): Pass :obj:`True`, if results may be cached on
+ the server side only for the user that sent the query. By default,
+ results may be returned to any user who sends the same query.
next_offset (:obj:`str`, optional): Pass the offset that a client should send in the
next query with the same text to receive more results. Pass an empty string if
there are no more results or if you don't support pagination. Offset length can't
exceed 64 bytes.
switch_pm_text (:obj:`str`, optional): If passed, clients will display a button with
specified text that switches the user to a private chat with the bot and sends the
- bot a start message with the parameter switch_pm_parameter.
+ bot a start message with the parameter ``switch_pm_parameter``.
switch_pm_parameter (:obj:`str`, optional): Deep-linking parameter for the /start
message sent to the bot when user presses the switch button. 1-64 characters,
only A-Z, a-z, 0-9, _ and - are allowed.
+ current_offset (:obj:`str`, optional): The :attr:`telegram.InlineQuery.offset` of
+ the inline query to answer. If passed, PTB will automatically take care of
+ the pagination for you, i.e. pass the correct ``next_offset`` and truncate the
+ results list/get the results from the callable you passed.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- he read timeout from the server (instead of the one specified during creation of
+ the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Example:
An inline bot that sends YouTube videos can ask the user to connect the bot to their
@@ -1438,17 +2264,54 @@ def answer_inline_query(self,
where they wanted to use the bot's inline capabilities.
Returns:
- :obj:`bool` On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/answerInlineQuery'.format(self.base_url)
- results = [res.to_dict() for res in results]
-
- data = {'inline_query_id': inline_query_id, 'results': results}
+ @no_type_check
+ def _set_defaults(res):
+ # pylint: disable=W0212
+ if hasattr(res, 'parse_mode') and res.parse_mode == DEFAULT_NONE:
+ if self.defaults:
+ res.parse_mode = self.defaults.parse_mode
+ else:
+ res.parse_mode = None
+ if hasattr(res, 'input_message_content') and res.input_message_content:
+ if (
+ hasattr(res.input_message_content, 'parse_mode')
+ and res.input_message_content.parse_mode == DEFAULT_NONE
+ ):
+ if self.defaults:
+ res.input_message_content.parse_mode = DefaultValue.get_value(
+ self.defaults.parse_mode
+ )
+ else:
+ res.input_message_content.parse_mode = None
+ if (
+ hasattr(res.input_message_content, 'disable_web_page_preview')
+ and res.input_message_content.disable_web_page_preview == DEFAULT_NONE
+ ):
+ if self.defaults:
+ res.input_message_content.disable_web_page_preview = (
+ DefaultValue.get_value(self.defaults.disable_web_page_preview)
+ )
+ else:
+ res.input_message_content.disable_web_page_preview = None
+
+ effective_results, next_offset = self._effective_inline_results(
+ results=results, next_offset=next_offset, current_offset=current_offset
+ )
+
+ # Apply defaults
+ for result in effective_results:
+ _set_defaults(result)
+
+ results_dicts = [res.to_dict() for res in effective_results]
+
+ data: JSONDict = {'inline_query_id': inline_query_id, 'results': results_dicts}
if cache_time or cache_time == 0:
data['cache_time'] = cache_time
@@ -1461,14 +2324,22 @@ def answer_inline_query(self,
if switch_pm_parameter:
data['switch_pm_parameter'] = switch_pm_parameter
- data.update(kwargs)
-
- result = self._request.post(url, data, timeout=timeout)
-
- return result
+ return self._post( # type: ignore[return-value]
+ 'answerInlineQuery',
+ data,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
@log
- def get_user_profile_photos(self, user_id, offset=None, limit=100, timeout=None, **kwargs):
+ def get_user_profile_photos(
+ self,
+ user_id: Union[str, int],
+ offset: int = None,
+ limit: int = 100,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Optional[UserProfilePhotos]:
"""Use this method to get a list of profile pictures for a user.
Args:
@@ -1476,44 +2347,55 @@ def get_user_profile_photos(self, user_id, offset=None, limit=100, timeout=None,
offset (:obj:`int`, optional): Sequential number of the first photo to be returned.
By default, all photos are returned.
limit (:obj:`int`, optional): Limits the number of photos to be retrieved. Values
- between 1-100 are accepted. Defaults to 100.
+ between 1-100 are accepted. Defaults to ``100``.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.UserProfilePhotos`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/getUserProfilePhotos'.format(self.base_url)
-
- data = {'user_id': user_id}
+ data: JSONDict = {'user_id': user_id}
if offset is not None:
data['offset'] = offset
if limit:
data['limit'] = limit
- data.update(kwargs)
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('getUserProfilePhotos', data, timeout=timeout, api_kwargs=api_kwargs)
- return UserProfilePhotos.de_json(result, self)
+ return UserProfilePhotos.de_json(result, self) # type: ignore[return-value, arg-type]
@log
- def get_file(self, file_id, timeout=None, **kwargs):
+ def get_file(
+ self,
+ file_id: Union[
+ str, Animation, Audio, ChatPhoto, Document, PhotoSize, Sticker, Video, VideoNote, Voice
+ ],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> File:
"""
Use this method to get basic info about a file and prepare it for downloading. For the
moment, bots can download files of up to 20MB in size. The file can then be downloaded
- with :attr:`telegram.File.download`. It is guaranteed that the link will be
+ with :meth:`telegram.File.download`. It is guaranteed that the link will be
valid for at least 1 hour. When the link expires, a new one can be requested by
calling get_file again.
+ Note:
+ This function may not preserve the original file name and MIME type.
+ You should save the file's MIME type and name (if available) when the File object
+ is received.
+
Args:
- file_id (:obj:`str` | :class:`telegram.Audio` | :class:`telegram.Document` | \
+ file_id (:obj:`str` | :class:`telegram.Animation` | :class:`telegram.Audio` | \
+ :class:`telegram.ChatPhoto` | :class:`telegram.Document` | \
:class:`telegram.PhotoSize` | :class:`telegram.Sticker` | \
:class:`telegram.Video` | :class:`telegram.VideoNote` | \
:class:`telegram.Voice`):
@@ -1522,135 +2404,277 @@ def get_file(self, file_id, timeout=None, **kwargs):
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.File`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/getFile'.format(self.base_url)
-
try:
- file_id = file_id.file_id
+ file_id = file_id.file_id # type: ignore[union-attr]
except AttributeError:
pass
- data = {'file_id': file_id}
- data.update(kwargs)
+ data: JSONDict = {'file_id': file_id}
+
+ result = self._post('getFile', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ if result.get('file_path') and not is_local_file( # type: ignore[union-attr]
+ result['file_path'] # type: ignore[index]
+ ):
+ result['file_path'] = '{}/{}'.format( # type: ignore[index]
+ self.base_file_url, result['file_path'] # type: ignore[index]
+ )
- result = self._request.post(url, data, timeout=timeout)
+ return File.de_json(result, self) # type: ignore[return-value, arg-type]
- if result.get('file_path'):
- result['file_path'] = '%s/%s' % (self.base_file_url, result['file_path'])
+ @log
+ def kick_chat_member(
+ self,
+ chat_id: Union[str, int],
+ user_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ until_date: Union[int, datetime] = None,
+ api_kwargs: JSONDict = None,
+ revoke_messages: bool = None,
+ ) -> bool:
+ """
+ Deprecated, use :func:`~telegram.Bot.ban_chat_member` instead.
- return File.de_json(result, self)
+ .. deprecated:: 13.7
+
+ """
+ warnings.warn(
+ '`bot.kick_chat_member` is deprecated. Use `bot.ban_chat_member` instead.',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+ return self.ban_chat_member(
+ chat_id=chat_id,
+ user_id=user_id,
+ timeout=timeout,
+ until_date=until_date,
+ api_kwargs=api_kwargs,
+ revoke_messages=revoke_messages,
+ )
@log
- def kick_chat_member(self, chat_id, user_id, timeout=None, until_date=None, **kwargs):
+ def ban_chat_member(
+ self,
+ chat_id: Union[str, int],
+ user_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ until_date: Union[int, datetime] = None,
+ api_kwargs: JSONDict = None,
+ revoke_messages: bool = None,
+ ) -> bool:
"""
- Use this method to kick a user from a group or a supergroup. In the case of supergroups,
- the user will not be able to return to the group on their own using invite links, etc.,
- unless unbanned first. The bot must be an administrator in the group for this to work.
+ Use this method to ban a user from a group, supergroup or a channel. In the case of
+ supergroups and channels, the user will not be able to return to the group on their own
+ using invite links, etc., unless unbanned first. The bot must be an administrator in the
+ chat for this to work and must have the appropriate admin rights.
+
+ .. versionadded:: 13.7
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username
+ of the target supergroup or channel (in the format ``@channelusername``).
user_id (:obj:`int`): Unique identifier of the target user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the user will
be unbanned, unix time. If user is banned for more than 366 days or less than 30
- seconds from the current time they are considered to be banned forever.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
-
- Note:
- In regular groups (non-supergroups), this method will only work if the
- 'All Members Are Admins' setting is off in the target group. Otherwise
- members may only be removed by the group's creator or by the member that added them.
+ seconds from the current time they are considered to be banned forever. Applied
+ for supergroups and channels only.
+ For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
+ bot will be used.
+ revoke_messages (:obj:`bool`, optional): Pass :obj:`True` to delete all messages from
+ the chat for the user that is being removed. If :obj:`False`, the user will be able
+ to see messages in the group that were sent before the user was removed.
+ Always :obj:`True` for supergroups and channels.
+
+ .. versionadded:: 13.4
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool` On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/kickChatMember'.format(self.base_url)
-
- data = {'chat_id': chat_id, 'user_id': user_id}
- data.update(kwargs)
+ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id}
if until_date is not None:
if isinstance(until_date, datetime):
- until_date = to_timestamp(until_date)
+ until_date = to_timestamp(
+ until_date, tzinfo=self.defaults.tzinfo if self.defaults else None
+ )
data['until_date'] = until_date
- result = self._request.post(url, data, timeout=timeout)
+ if revoke_messages is not None:
+ data['revoke_messages'] = revoke_messages
- return result
+ result = self._post('banChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
@log
- def unban_chat_member(self, chat_id, user_id, timeout=None, **kwargs):
- """Use this method to unban a previously kicked user in a supergroup.
+ def ban_chat_sender_chat(
+ self,
+ chat_id: Union[str, int],
+ sender_chat_id: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to ban a channel chat in a supergroup or a channel. Until the chat is
+ unbanned, the owner of the banned chat won't be able to send messages on behalf of **any of
+ their channels**. The bot must be an administrator in the supergroup or channel for this
+ to work and must have the appropriate administrator rights.
+
+ .. versionadded:: 13.9
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username
+ of the target supergroup or channel (in the format ``@channelusername``).
+ sender_chat_id (:obj:`int`): Unique identifier of the target sender chat.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'chat_id': chat_id, 'sender_chat_id': sender_chat_id}
- The user will not return to the group automatically, but will be able to join via link,
- etc. The bot must be an administrator in the group for this to work.
+ result = self._post('banChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
+
+ @log
+ def unban_chat_member(
+ self,
+ chat_id: Union[str, int],
+ user_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ only_if_banned: bool = None,
+ ) -> bool:
+ """Use this method to unban a previously kicked user in a supergroup or channel.
+
+ The user will *not* return to the group or channel automatically, but will be able to join
+ via link, etc. The bot must be an administrator for this to work. By default, this method
+ guarantees that after the call the user is not a member of the chat, but will be able to
+ join it. So if the user is a member of the chat they will also be *removed* from the chat.
+ If you don't want this, use the parameter :attr:`only_if_banned`.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ of the target supergroup or channel (in the format ``@channelusername``).
user_id (:obj:`int`): Unique identifier of the target user.
+ only_if_banned (:obj:`bool`, optional): Do nothing if the user is not banned.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool` On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/unbanChatMember'.format(self.base_url)
+ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id}
- data = {'chat_id': chat_id, 'user_id': user_id}
- data.update(kwargs)
+ if only_if_banned is not None:
+ data['only_if_banned'] = only_if_banned
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('unbanChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
+
+ @log
+ def unban_chat_sender_chat(
+ self,
+ chat_id: Union[str, int],
+ sender_chat_id: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Use this method to unban a previously banned channel in a supergroup or channel.
+ The bot must be an administrator for this to work and must have the
+ appropriate administrator rights.
+
+ .. versionadded:: 13.9
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target supergroup or channel (in the format ``@channelusername``).
+ sender_chat_id (:obj:`int`): Unique identifier of the target sender chat.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'chat_id': chat_id, 'sender_chat_id': sender_chat_id}
+
+ result = self._post('unbanChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
@log
- def answer_callback_query(self,
- callback_query_id,
- text=None,
- show_alert=False,
- url=None,
- cache_time=None,
- timeout=None,
- **kwargs):
+ def answer_callback_query(
+ self,
+ callback_query_id: str,
+ text: str = None,
+ show_alert: bool = False,
+ url: str = None,
+ cache_time: int = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""
Use this method to send answers to callback queries sent from inline keyboards. The answer
will be displayed to the user as a notification at the top of the chat screen or as an
alert.
Alternatively, the user can be redirected to the specified Game URL. For this option to
- work, you must first create a game for your bot via BotFather and accept the terms.
- Otherwise, you may use links like t.me/your_bot?start=XXXX that open your bot with
- a parameter.
+ work, you must first create a game for your bot via `@BotFather `_
+ and accept the terms. Otherwise, you may use links like t.me/your_bot?start=XXXX that open
+ your bot with a parameter.
Args:
callback_query_id (:obj:`str`): Unique identifier for the query to be answered.
text (:obj:`str`, optional): Text of the notification. If not specified, nothing will
be shown to the user, 0-200 characters.
- show_alert (:obj:`bool`, optional): If true, an alert will be shown by the client
- instead of a notification at the top of the chat screen. Defaults to false.
+ show_alert (:obj:`bool`, optional): If :obj:`True`, an alert will be shown by the
+ client instead of a notification at the top of the chat screen. Defaults to
+ :obj:`False`.
url (:obj:`str`, optional): URL that will be opened by the user's client. If you have
- created a Game and accepted the conditions via @Botfather, specify the URL that
+ created a Game and accepted the conditions via
+ `@BotFather `_, specify the URL that
opens your game - note that this will only work if the query comes from a callback
game button. Otherwise, you may use links like t.me/your_bot?start=XXXX that open
your bot with a parameter.
@@ -1659,18 +2683,17 @@ def answer_callback_query(self,
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool` On success, ``True`` is returned.
+ :obj:`bool` On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url_ = '{0}/answerCallbackQuery'.format(self.base_url)
-
- data = {'callback_query_id': callback_query_id}
+ data: JSONDict = {'callback_query_id': callback_query_id}
if text:
data['text'] = text
@@ -1680,60 +2703,65 @@ def answer_callback_query(self,
data['url'] = url
if cache_time is not None:
data['cache_time'] = cache_time
- data.update(kwargs)
- result = self._request.post(url_, data, timeout=timeout)
+ result = self._post('answerCallbackQuery', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- @message
- def edit_message_text(self,
- text,
- chat_id=None,
- message_id=None,
- inline_message_id=None,
- parse_mode=None,
- disable_web_page_preview=None,
- reply_markup=None,
- timeout=None,
- **kwargs):
+ def edit_message_text(
+ self,
+ text: str,
+ chat_id: Union[str, int] = None,
+ message_id: int = None,
+ inline_message_id: int = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ ) -> Union[Message, bool]:
"""
- Use this method to edit text and game messages sent by the bot or via the bot (for inline
- bots).
+ Use this method to edit text and game messages.
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target channel (in the format @channelusername).
+ chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
+ specified. Unique identifier for the target chat or username of the target channel
+ (in the format ``@channelusername``)
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
- Identifier of the sent message.
+ Identifier of the message to edit.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
specified. Identifier of the inline message.
- text (:obj:`str`): New text of the message.
- parse_mode (:obj:`str`): Send Markdown or HTML, if you want Telegram apps to show bold,
- italic, fixed-width text or inline URLs in your bot's message. See the constants in
- :class:`telegram.ParseMode` for the available modes.
+ text (:obj:`str`): New text of the message, 1-4096 characters after entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
+ show bold, italic, fixed-width text or inline URLs in your bot's message. See the
+ constants in :class:`telegram.ParseMode` for the available modes.
+ entities (List[:class:`telegram.MessageEntity`], optional): List of special entities
+ that appear in message text, which can be specified instead of :attr:`parse_mode`.
disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in
this message.
- reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
- JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
- to remove reply keyboard or to force a reply from the user.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized
+ object for an inline keyboard.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :class:`telegram.Message`: On success, if edited message is sent by the bot, the
- edited Message is returned, otherwise ``True`` is returned.
+ :class:`telegram.Message`: On success, if edited message is not an inline message, the
+ edited message is returned, otherwise :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/editMessageText'.format(self.base_url)
-
- data = {'text': text}
+ data: JSONDict = {
+ 'text': text,
+ 'parse_mode': parse_mode,
+ 'disable_web_page_preview': disable_web_page_preview,
+ }
if chat_id:
data['chat_id'] = chat_id
@@ -1741,68 +2769,77 @@ def edit_message_text(self,
data['message_id'] = message_id
if inline_message_id:
data['inline_message_id'] = inline_message_id
- if parse_mode:
- data['parse_mode'] = parse_mode
- if disable_web_page_preview:
- data['disable_web_page_preview'] = disable_web_page_preview
-
- return url, data
-
- @log
- @message
- def edit_message_caption(self,
- chat_id=None,
- message_id=None,
- inline_message_id=None,
- caption=None,
- reply_markup=None,
- timeout=None,
- parse_mode=None,
- **kwargs):
- """
- Use this method to edit captions of messages sent by the bot or via the bot
- (for inline bots).
+ if entities:
+ data['entities'] = [me.to_dict() for me in entities]
+
+ return self._message(
+ 'editMessageText',
+ data,
+ timeout=timeout,
+ reply_markup=reply_markup,
+ api_kwargs=api_kwargs,
+ )
+
+ @log
+ def edit_message_caption(
+ self,
+ chat_id: Union[str, int] = None,
+ message_id: int = None,
+ inline_message_id: int = None,
+ caption: str = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ ) -> Union[Message, bool]:
+ """
+ Use this method to edit captions of messages.
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
+ specified. Unique identifier for the target chat or username of the target channel
+ (in the format ``@channelusername``)
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
- Identifier of the sent message.
+ Identifier of the message to edit.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
specified. Identifier of the inline message.
- caption (:obj:`str`, optional): New caption of the message.
+ caption (:obj:`str`, optional): New caption of the message, 0-1024 characters after
+ entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
- JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
- to remove reply keyboard or to force a reply from the user.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in message text, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized
+ object for an inline keyboard.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :class:`telegram.Message`: On success, if edited message is sent by the bot, the
- edited Message is returned, otherwise ``True`` is returned.
+ :class:`telegram.Message`: On success, if edited message is not an inline message, the
+ edited message is returned, otherwise :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
if inline_message_id is None and (chat_id is None or message_id is None):
raise ValueError(
'edit_message_caption: Both chat_id and message_id are required when '
- 'inline_message_id is not specified')
+ 'inline_message_id is not specified'
+ )
- url = '{0}/editMessageCaption'.format(self.base_url)
-
- data = {}
+ data: JSONDict = {'parse_mode': parse_mode}
if caption:
data['caption'] = caption
- if parse_mode:
- data['parse_mode'] = parse_mode
+ if caption_entities:
+ data['caption_entities'] = [me.to_dict() for me in caption_entities]
if chat_id:
data['chat_id'] = chat_id
if message_id:
@@ -1810,51 +2847,64 @@ def edit_message_caption(self,
if inline_message_id:
data['inline_message_id'] = inline_message_id
- return url, data
+ return self._message(
+ 'editMessageCaption',
+ data,
+ timeout=timeout,
+ reply_markup=reply_markup,
+ api_kwargs=api_kwargs,
+ )
@log
- @message
- def edit_message_media(self,
- chat_id=None,
- message_id=None,
- inline_message_id=None,
- media=None,
- reply_markup=None,
- timeout=None,
- **kwargs):
- """Use this method to edit audio, document, photo, or video messages. If a message is a
- part of a message album, then it can be edited only to a photo or a video. Otherwise,
- message type can be changed arbitrarily. When inline message is edited, new file can't be
- uploaded. Use previously uploaded file via its file_id or specify a URL. On success, if the
- edited message was sent by the bot, the edited Message is returned, otherwise True is
- returned.
+ def edit_message_media(
+ self,
+ chat_id: Union[str, int] = None,
+ message_id: int = None,
+ inline_message_id: int = None,
+ media: 'InputMedia' = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union[Message, bool]:
+ """
+ Use this method to edit animation, audio, document, photo, or video messages. If a message
+ is part of a message album, then it can be edited only to an audio for audio albums, only
+ to a document for document albums and to a photo or a video otherwise. When an inline
+ message is edited, a new file can't be uploaded. Use a previously uploaded file via its
+ ``file_id`` or specify a URL.
Args:
- chat_id (:obj:`int` | :obj:`str`, optional): Unique identifier for the target chat or
- username of the target`channel (in the format @channelusername).
+ chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
+ specified. Unique identifier for the target chat or username of the target channel
+ (in the format ``@channelusername``).
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
- Identifier of the sent message.
+ Identifier of the message to edit.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
specified. Identifier of the inline message.
media (:class:`telegram.InputMedia`): An object for a new media content
of the message.
- reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
- JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
- to remove reply keyboard or to force a reply from the user.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized
+ object for an inline keyboard.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- """
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise :obj:`True` is returned.
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
if inline_message_id is None and (chat_id is None or message_id is None):
raise ValueError(
- 'edit_message_caption: Both chat_id and message_id are required when '
- 'inline_message_id is not specified')
-
- url = '{0}/editMessageMedia'.format(self.base_url)
+ 'edit_message_media: Both chat_id and message_id are required when '
+ 'inline_message_id is not specified'
+ )
- data = {'media': media}
+ data: JSONDict = {'media': media}
if chat_id:
data['chat_id'] = chat_id
@@ -1863,52 +2913,59 @@ def edit_message_media(self,
if inline_message_id:
data['inline_message_id'] = inline_message_id
- return url, data
+ return self._message(
+ 'editMessageMedia',
+ data,
+ timeout=timeout,
+ reply_markup=reply_markup,
+ api_kwargs=api_kwargs,
+ )
@log
- @message
- def edit_message_reply_markup(self,
- chat_id=None,
- message_id=None,
- inline_message_id=None,
- reply_markup=None,
- timeout=None,
- **kwargs):
+ def edit_message_reply_markup(
+ self,
+ chat_id: Union[str, int] = None,
+ message_id: int = None,
+ inline_message_id: int = None,
+ reply_markup: Optional['InlineKeyboardMarkup'] = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union[Message, bool]:
"""
Use this method to edit only the reply markup of messages sent by the bot or via the bot
(for inline bots).
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
+ specified. Unique identifier for the target chat or username of the target channel
+ (in the format ``@channelusername``).
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
- Identifier of the sent message.
+ Identifier of the message to edit.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
specified. Identifier of the inline message.
- reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
- JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
- to remove reply keyboard or to force a reply from the user.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized
+ object for an inline keyboard.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :class:`telegram.Message`: On success, if edited message is sent by the bot, the
- editedMessage is returned, otherwise ``True`` is returned.
+ :class:`telegram.Message`: On success, if edited message is not an inline message, the
+ edited message is returned, otherwise :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
if inline_message_id is None and (chat_id is None or message_id is None):
raise ValueError(
'edit_message_reply_markup: Both chat_id and message_id are required when '
- 'inline_message_id is not specified')
-
- url = '{0}/editMessageReplyMarkup'.format(self.base_url)
+ 'inline_message_id is not specified'
+ )
- data = {}
+ data: JSONDict = {}
if chat_id:
data['chat_id'] = chat_id
@@ -1917,16 +2974,24 @@ def edit_message_reply_markup(self,
if inline_message_id:
data['inline_message_id'] = inline_message_id
- return url, data
+ return self._message(
+ 'editMessageReplyMarkup',
+ data,
+ timeout=timeout,
+ reply_markup=reply_markup,
+ api_kwargs=api_kwargs,
+ )
@log
- def get_updates(self,
- offset=None,
- limit=100,
- timeout=0,
- read_latency=2.,
- allowed_updates=None,
- **kwargs):
+ def get_updates(
+ self,
+ offset: int = None,
+ limit: int = 100,
+ timeout: float = 0,
+ read_latency: float = 2.0,
+ allowed_updates: List[str] = None,
+ api_kwargs: JSONDict = None,
+ ) -> List[Update]:
"""Use this method to receive incoming updates using long polling.
Args:
@@ -1934,25 +2999,30 @@ def get_updates(self,
greater by one than the highest among the identifiers of previously received
updates. By default, updates starting with the earliest unconfirmed update are
returned. An update is considered confirmed as soon as getUpdates is called with an
- offset higher than its update_id. The negative offset can be specified to retrieve
- updates starting from -offset update from the end of the updates queue. All
- previous updates will forgotten.
+ offset higher than its :attr:`telegram.Update.update_id`. The negative offset can
+ be specified to retrieve updates starting from -offset update from the end of the
+ updates queue. All previous updates will forgotten.
limit (:obj:`int`, optional): Limits the number of updates to be retrieved. Values
- between 1-100 are accepted. Defaults to 100.
- timeout (:obj:`int`, optional): Timeout in seconds for long polling. Defaults to 0,
+ between 1-100 are accepted. Defaults to ``100``.
+ timeout (:obj:`int`, optional): Timeout in seconds for long polling. Defaults to ``0``,
i.e. usual short polling. Should be positive, short polling should be used for
testing purposes only.
- allowed_updates (List[:obj:`str`]), optional): List the types of updates you want your
- bot to receive. For example, specify ["message", "edited_channel_post",
- "callback_query"] to only receive updates of these types. See
- :class:`telegram.Update` for a complete list of available update types.
- Specify an empty list to receive all updates regardless of type (default). If not
- specified, the previous setting will be used. Please note that this parameter
- doesn't affect updates created before the call to the get_updates, so unwanted
- updates may be received for a short period of time.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
-
- Notes:
+ read_latency (:obj:`float` | :obj:`int`, optional): Grace time in seconds for receiving
+ the reply from server. Will be added to the ``timeout`` value and used as the read
+ timeout from server. Defaults to ``2``.
+ allowed_updates (List[:obj:`str`]), optional): A JSON-serialized list the types of
+ updates you want your bot to receive. For example, specify ["message",
+ "edited_channel_post", "callback_query"] to only receive updates of these types.
+ See :class:`telegram.Update` for a complete list of available update types.
+ Specify an empty list to receive all updates except
+ :attr:`telegram.Update.chat_member` (default). If not specified, the previous
+ setting will be used. Please note that this parameter doesn't affect updates
+ created before the call to the get_updates, so unwanted updates may be received for
+ a short period of time.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Note:
1. This method will not work if an outgoing webhook is set up.
2. In order to avoid getting duplicate updates, recalculate offset after each
server response.
@@ -1962,12 +3032,10 @@ def get_updates(self,
List[:class:`telegram.Update`]
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/getUpdates'.format(self.base_url)
-
- data = {'timeout': timeout}
+ data: JSONDict = {'timeout': timeout}
if offset:
data['offset'] = offset
@@ -1975,39 +3043,50 @@ def get_updates(self,
data['limit'] = limit
if allowed_updates is not None:
data['allowed_updates'] = allowed_updates
- data.update(kwargs)
# Ideally we'd use an aggressive read timeout for the polling. However,
# * Short polling should return within 2 seconds.
# * Long polling poses a different problem: the connection might have been dropped while
# waiting for the server to return and there's no way of knowing the connection had been
# dropped in real time.
- result = self._request.post(url, data, timeout=float(read_latency) + float(timeout))
+ result = cast(
+ List[JSONDict],
+ self._post(
+ 'getUpdates',
+ data,
+ timeout=float(read_latency) + float(timeout),
+ api_kwargs=api_kwargs,
+ ),
+ )
if result:
self.logger.debug('Getting updates: %s', [u['update_id'] for u in result])
else:
self.logger.debug('No new updates found.')
- return [Update.de_json(u, self) for u in result]
+ return Update.de_list(result, self) # type: ignore[return-value]
@log
- def set_webhook(self,
- url=None,
- certificate=None,
- timeout=None,
- max_connections=40,
- allowed_updates=None,
- **kwargs):
+ def set_webhook(
+ self,
+ url: str = None,
+ certificate: FileInput = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ max_connections: int = 40,
+ allowed_updates: List[str] = None,
+ api_kwargs: JSONDict = None,
+ ip_address: str = None,
+ drop_pending_updates: bool = None,
+ ) -> bool:
"""
Use this method to specify a url and receive incoming updates via an outgoing webhook.
- Whenever there is an update for the bot, we will send an HTTPS POST request to the
+ Whenever there is an update for the bot, Telegram will send an HTTPS POST request to the
specified url, containing a JSON-serialized Update. In case of an unsuccessful request,
- we will give up after a reasonable amount of attempts.
+ Telegram will give up after a reasonable amount of attempts.
- If you'd like to make sure that the Webhook request comes from Telegram, we recommend
- using a secret path in the URL, e.g. https://www.example.com/. Since nobody else
- knows your bot's token, you can be pretty sure it's us.
+ If you'd like to make sure that the Webhook request comes from Telegram, Telegram
+ recommends using a secret path in the URL, e.g. https://www.example.com/. Since
+ nobody else knows your bot's token, you can be pretty sure it's us.
Note:
The certificate argument should be a file from disk ``open(filename, 'rb')``.
@@ -2018,249 +3097,301 @@ def set_webhook(self,
certificate (:obj:`filelike`): Upload your public key certificate so that the root
certificate in use can be checked. See our self-signed guide for details.
(https://goo.gl/rw7w6Y)
+ ip_address (:obj:`str`, optional): The fixed IP address which will be used to send
+ webhook requests instead of the IP address resolved through DNS.
max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS
- connections to the webhook for update delivery, 1-100. Defaults to 40. Use lower
- values to limit the load on your bot's server, and higher values to increase your
- bot's throughput.
- allowed_updates (List[:obj:`str`], optional): List the types of updates you want your
- bot to receive. For example, specify ["message", "edited_channel_post",
- "callback_query"] to only receive updates of these types. See
- :class:`telegram.Update` for a complete list of available update types. Specify an
- empty list to receive all updates regardless of type (default). If not specified,
- the previous setting will be used. Please note that this parameter doesn't affect
- updates created before the call to the set_webhook, so unwanted updates may be
- received for a short period of time.
+ connections to the webhook for update delivery, 1-100. Defaults to ``40``. Use
+ lower values to limit the load on your bot's server, and higher values to increase
+ your bot's throughput.
+ allowed_updates (List[:obj:`str`], optional): A JSON-serialized list the types of
+ updates you want your bot to receive. For example, specify ["message",
+ "edited_channel_post", "callback_query"] to only receive updates of these types.
+ See :class:`telegram.Update` for a complete list of available update types.
+ Specify an empty list to receive all updates except
+ :attr:`telegram.Update.chat_member` (default). If not specified, the previous
+ setting will be used. Please note that this parameter doesn't affect updates
+ created before the call to the set_webhook, so unwanted updates may be received for
+ a short period of time.
+ drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending
+ updates.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Note:
- 1. You will not be able to receive updates using get_updates for as long as an outgoing
- webhook is set up.
+ 1. You will not be able to receive updates using :meth:`get_updates` for long as an
+ outgoing webhook is set up.
2. To use a self-signed certificate, you need to upload your public key certificate
using certificate parameter. Please upload as InputFile, sending a String will not
work.
- 3. Ports currently supported for Webhooks: 443, 80, 88, 8443.
+ 3. Ports currently supported for Webhooks: ``443``, ``80``, ``88``, ``8443``.
+
+ If you're having any trouble setting up webhooks, please check out this `guide to
+ Webhooks`_.
Returns:
- :obj:`bool` On success, ``True`` is returned.
+ :obj:`bool` On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
-
- """
- url_ = '{0}/setWebhook'.format(self.base_url)
-
- # Backwards-compatibility: 'url' used to be named 'webhook_url'
- if 'webhook_url' in kwargs: # pragma: no cover
- warnings.warn("The 'webhook_url' parameter has been renamed to 'url' in accordance "
- "with the API")
+ :class:`telegram.error.TelegramError`
- if url is not None:
- raise ValueError("The parameters 'url' and 'webhook_url' are mutually exclusive")
+ .. _`guide to Webhooks`: https://core.telegram.org/bots/webhooks
- url = kwargs['webhook_url']
- del kwargs['webhook_url']
-
- data = {}
+ """
+ data: JSONDict = {}
if url is not None:
data['url'] = url
if certificate:
- if InputFile.is_file(certificate):
- certificate = InputFile(certificate)
- data['certificate'] = certificate
+ data['certificate'] = parse_file_input(certificate)
if max_connections is not None:
data['max_connections'] = max_connections
if allowed_updates is not None:
data['allowed_updates'] = allowed_updates
- data.update(kwargs)
+ if ip_address:
+ data['ip_address'] = ip_address
+ if drop_pending_updates:
+ data['drop_pending_updates'] = drop_pending_updates
- result = self._request.post(url_, data, timeout=timeout)
+ result = self._post('setWebhook', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def delete_webhook(self, timeout=None, **kwargs):
+ def delete_webhook(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ drop_pending_updates: bool = None,
+ ) -> bool:
"""
Use this method to remove webhook integration if you decide to switch back to
- getUpdates. Requires no parameters.
+ :meth:`get_updates()`.
Args:
+ drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending
+ updates.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool` On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/deleteWebhook'.format(self.base_url)
+ data = {}
- data = kwargs
+ if drop_pending_updates:
+ data['drop_pending_updates'] = drop_pending_updates
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('deleteWebhook', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def leave_chat(self, chat_id, timeout=None, **kwargs):
+ def leave_chat(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Use this method for your bot to leave a group, supergroup or channel.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ of the target supergroup or channel (in the format ``@channelusername``).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool` On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/leaveChat'.format(self.base_url)
+ data: JSONDict = {'chat_id': chat_id}
- data = {'chat_id': chat_id}
- data.update(kwargs)
+ result = self._post('leaveChat', data, timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.post(url, data, timeout=timeout)
-
- return result
+ return result # type: ignore[return-value]
@log
- def get_chat(self, chat_id, timeout=None, **kwargs):
+ def get_chat(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Chat:
"""
Use this method to get up to date information about the chat (current name of the user for
one-on-one conversations, current username of a user, group or channel, etc.).
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ of the target supergroup or channel (in the format ``@channelusername``).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Chat`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/getChat'.format(self.base_url)
+ data: JSONDict = {'chat_id': chat_id}
- data = {'chat_id': chat_id}
- data.update(kwargs)
+ result = self._post('getChat', data, timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.post(url, data, timeout=timeout)
-
- return Chat.de_json(result, self)
+ return Chat.de_json(result, self) # type: ignore[return-value, arg-type]
@log
- def get_chat_administrators(self, chat_id, timeout=None, **kwargs):
+ def get_chat_administrators(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> List[ChatMember]:
"""
- Use this method to get a list of administrators in a chat. On success, returns an Array of
- ChatMember objects that contains information about all chat administrators except other
- bots. If the chat is a group or a supergroup and no administrators were appointed,
- only the creator will be returned.
+ Use this method to get a list of administrators in a chat.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ of the target supergroup or channel (in the format ``@channelusername``).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- List[:class:`telegram.ChatMember`]
+ List[:class:`telegram.ChatMember`]: On success, returns a list of ``ChatMember``
+ objects that contains information about all chat administrators except
+ other bots. If the chat is a group or a supergroup and no administrators were
+ appointed, only the creator will be returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/getChatAdministrators'.format(self.base_url)
+ data: JSONDict = {'chat_id': chat_id}
+
+ result = self._post('getChatAdministrators', data, timeout=timeout, api_kwargs=api_kwargs)
- data = {'chat_id': chat_id}
- data.update(kwargs)
+ return ChatMember.de_list(result, self) # type: ignore
- result = self._request.post(url, data, timeout=timeout)
+ @log
+ def get_chat_members_count(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> int:
+ """
+ Deprecated, use :func:`~telegram.Bot.get_chat_member_count` instead.
- return [ChatMember.de_json(x, self) for x in result]
+ .. deprecated:: 13.7
+ """
+ warnings.warn(
+ '`bot.get_chat_members_count` is deprecated. '
+ 'Use `bot.get_chat_member_count` instead.',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+ return self.get_chat_member_count(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs)
@log
- def get_chat_members_count(self, chat_id, timeout=None, **kwargs):
- """Use this method to get the number of members in a chat
+ def get_chat_member_count(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> int:
+ """Use this method to get the number of members in a chat.
+
+ .. versionadded:: 13.7
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ of the target supergroup or channel (in the format ``@channelusername``).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- int: Number of members in the chat.
+ :obj:`int`: Number of members in the chat.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/getChatMembersCount'.format(self.base_url)
+ data: JSONDict = {'chat_id': chat_id}
- data = {'chat_id': chat_id}
- data.update(kwargs)
+ result = self._post('getChatMemberCount', data, timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.post(url, data, timeout=timeout)
-
- return result
+ return result # type: ignore[return-value]
@log
- def get_chat_member(self, chat_id, user_id, timeout=None, **kwargs):
+ def get_chat_member(
+ self,
+ chat_id: Union[str, int],
+ user_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> ChatMember:
"""Use this method to get information about a member of a chat.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ of the target supergroup or channel (in the format ``@channelusername``).
user_id (:obj:`int`): Unique identifier of the target user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.ChatMember`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/getChatMember'.format(self.base_url)
+ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id}
- data = {'chat_id': chat_id, 'user_id': user_id}
- data.update(kwargs)
+ result = self._post('getChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.post(url, data, timeout=timeout)
-
- return ChatMember.de_json(result, self)
+ return ChatMember.de_json(result, self) # type: ignore[return-value, arg-type]
@log
- def set_chat_sticker_set(self, chat_id, sticker_set_name, timeout=None, **kwargs):
+ def set_chat_sticker_set(
+ self,
+ chat_id: Union[str, int],
+ sticker_set_name: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Use this method to set a new group sticker set for a supergroup.
The bot must be an administrator in the chat for this to work and must have the appropriate
admin rights. Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned
- in :attr:`get_chat` requests to check if the bot can use this method.
+ in :meth:`get_chat` requests to check if the bot can use this method.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
@@ -2270,27 +3401,29 @@ def set_chat_sticker_set(self, chat_id, sticker_set_name, timeout=None, **kwargs
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
-
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: True on success.
+ :obj:`bool`: On success, :obj:`True` is returned.
"""
+ data: JSONDict = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name}
- url = '{0}/setChatStickerSet'.format(self.base_url)
-
- data = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name}
-
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('setChatStickerSet', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def delete_chat_sticker_set(self, chat_id, timeout=None, **kwargs):
+ def delete_chat_sticker_set(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Use this method to delete a group sticker set from a supergroup. The bot must be an
administrator in the chat for this to work and must have the appropriate admin rights.
Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned in
- :attr:`get_chat` requests to check if the bot can use this method.
+ :meth:`get_chat` requests to check if the bot can use this method.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
@@ -2298,70 +3431,66 @@ def delete_chat_sticker_set(self, chat_id, timeout=None, **kwargs):
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: True on success.
+ :obj:`bool`: On success, :obj:`True` is returned.
"""
+ data: JSONDict = {'chat_id': chat_id}
- url = '{0}/deleteChatStickerSet'.format(self.base_url)
-
- data = {'chat_id': chat_id}
+ result = self._post('deleteChatStickerSet', data, timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.post(url, data, timeout=timeout)
-
- return result
+ return result # type: ignore[return-value]
- def get_webhook_info(self, timeout=None, **kwargs):
+ def get_webhook_info(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> WebhookInfo:
"""Use this method to get current webhook status. Requires no parameters.
- If the bot is using getUpdates, will return an object with the url field empty.
+ If the bot is using :meth:`get_updates`, will return an object with the
+ :attr:`telegram.WebhookInfo.url` field empty.
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.WebhookInfo`
"""
- url = '{0}/getWebhookInfo'.format(self.base_url)
+ result = self._post('getWebhookInfo', None, timeout=timeout, api_kwargs=api_kwargs)
- data = kwargs
-
- result = self._request.post(url, data, timeout=timeout)
-
- return WebhookInfo.de_json(result, self)
+ return WebhookInfo.de_json(result, self) # type: ignore[return-value, arg-type]
@log
- @message
- def set_game_score(self,
- user_id,
- score,
- chat_id=None,
- message_id=None,
- inline_message_id=None,
- force=None,
- disable_edit_message=None,
- timeout=None,
- **kwargs):
+ def set_game_score(
+ self,
+ user_id: Union[int, str],
+ score: int,
+ chat_id: Union[str, int] = None,
+ message_id: int = None,
+ inline_message_id: int = None,
+ force: bool = None,
+ disable_edit_message: bool = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union[Message, bool]:
"""
- Use this method to set the score of the specified user in a game. On success, if the
- message was sent by the bot, returns the edited Message, otherwise returns True. Returns
- an error, if the new score is not greater than the user's current score in the chat and
- force is False.
+ Use this method to set the score of the specified user in a game.
Args:
user_id (:obj:`int`): User identifier.
score (:obj:`int`): New score, must be non-negative.
- force (:obj:`bool`, optional): Pass True, if the high score is allowed to decrease.
- This can be useful when fixing mistakes or banning cheaters
- disable_edit_message (:obj:`bool`, optional): Pass True, if the game message should not
- be automatically edited to include the current scoreboard.
- chat_id (int|str, optional): Required if inline_message_id is not specified. Unique
- identifier for the target chat.
+ force (:obj:`bool`, optional): Pass :obj:`True`, if the high score is allowed to
+ decrease. This can be useful when fixing mistakes or banning cheaters.
+ disable_edit_message (:obj:`bool`, optional): Pass :obj:`True`, if the game message
+ should not be automatically edited to include the current scoreboard.
+ chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
+ specified. Unique identifier for the target chat.
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
Identifier of the sent message.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
@@ -2369,20 +3498,19 @@ def set_game_score(self,
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: The edited message, or if the message wasn't sent by the bot
- , ``True``.
+ , :obj:`True`.
Raises:
- :class:`telegram.TelegramError`: If the new score is not greater than the user's
- current score in the chat and force is False.
+ :class:`telegram.error.TelegramError`: If the new score is not greater than the user's
+ current score in the chat and force is :obj:`False`.
"""
- url = '{0}/setGameScore'.format(self.base_url)
-
- data = {'user_id': user_id, 'score': score}
+ data: JSONDict = {'user_id': user_id, 'score': score}
if chat_id:
data['chat_id'] = chat_id
@@ -2395,22 +3523,34 @@ def set_game_score(self,
if disable_edit_message is not None:
data['disable_edit_message'] = disable_edit_message
- return url, data
+ return self._message(
+ 'setGameScore',
+ data,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
@log
- def get_game_high_scores(self,
- user_id,
- chat_id=None,
- message_id=None,
- inline_message_id=None,
- timeout=None,
- **kwargs):
+ def get_game_high_scores(
+ self,
+ user_id: Union[int, str],
+ chat_id: Union[str, int] = None,
+ message_id: int = None,
+ inline_message_id: int = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> List[GameHighScore]:
"""
Use this method to get data for high score tables. Will return the score of the specified
- user and several of his neighbors in a game
+ user and several of their neighbors in a game.
+
+ Note:
+ This method will currently return scores for the target user, plus two of their
+ closest neighbors on each side. Will also return the top three users if the user and
+ his neighbors are not among them. Please note that this behavior is subject to change.
Args:
- user_id (:obj:`int`): User identifier.
+ user_id (:obj:`int`): Target user id.
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
specified. Unique identifier for the target chat.
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
@@ -2420,18 +3560,17 @@ def get_game_high_scores(self,
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
List[:class:`telegram.GameHighScore`]
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/getGameHighScores'.format(self.base_url)
-
- data = {'user_id': user_id}
+ data: JSONDict = {'user_id': user_id}
if chat_id:
data['chat_id'] = chat_id
@@ -2439,55 +3578,92 @@ def get_game_high_scores(self,
data['message_id'] = message_id
if inline_message_id:
data['inline_message_id'] = inline_message_id
- data.update(kwargs)
-
- result = self._request.post(url, data, timeout=timeout)
-
- return [GameHighScore.de_json(hs, self) for hs in result]
-
- @log
- @message
- def send_invoice(self,
- chat_id,
- title,
- description,
- payload,
- provider_token,
- start_parameter,
- currency,
- prices,
- 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=False,
- reply_to_message_id=None,
- reply_markup=None,
- provider_data=None,
- send_phone_number_to_provider=None,
- send_email_to_provider=None,
- timeout=None,
- **kwargs):
+
+ result = self._post('getGameHighScores', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return GameHighScore.de_list(result, self) # type: ignore
+
+ @log
+ def send_invoice(
+ self,
+ chat_id: Union[int, str],
+ title: str,
+ description: str,
+ payload: str,
+ provider_token: str,
+ currency: str,
+ prices: List['LabeledPrice'],
+ start_parameter: str = None,
+ photo_url: str = None,
+ photo_size: int = None,
+ photo_width: int = None,
+ photo_height: int = None,
+ need_name: bool = None,
+ need_phone_number: bool = None,
+ need_email: bool = None,
+ need_shipping_address: bool = None,
+ is_flexible: bool = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ provider_data: Union[str, object] = None,
+ send_phone_number_to_provider: bool = None,
+ send_email_to_provider: bool = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ max_tip_amount: int = None,
+ suggested_tip_amounts: List[int] = None,
+ protect_content: bool = None,
+ ) -> Message:
"""Use this method to send invoices.
+ Warning:
+ As of API 5.2 :attr:`start_parameter` is an optional argument and therefore the order
+ of the arguments had to be changed. Use keyword arguments to make sure that the
+ arguments are passed correctly.
+
+ .. versionchanged:: 13.5
+ As of Bot API 5.2, the parameter :attr:`start_parameter` is optional.
+
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat.
- title (:obj:`str`): Product name.
- description (:obj:`str`): Product description.
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ title (:obj:`str`): Product name, 1-32 characters.
+ description (:obj:`str`): Product description, 1-255 characters.
payload (:obj:`str`): Bot-defined invoice payload, 1-128 bytes. This will not be
displayed to the user, use for your internal processes.
- provider_token (:obj:`str`): Payments provider token, obtained via Botfather.
- start_parameter (:obj:`str`): Unique deep-linking parameter that can be used to
- generate this invoice when used as a start parameter.
+ provider_token (:obj:`str`): Payments provider token, obtained via
+ `@BotFather `_.
currency (:obj:`str`): Three-letter ISO 4217 currency code.
- prices (List[:class:`telegram.LabeledPrice`)]: Price breakdown, a list of components
- (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.).
- provider_data (:obj:`str` | :obj:`object`, optional): JSON-encoded data about the
+ prices (List[:class:`telegram.LabeledPrice`)]: Price breakdown, a JSON-serialized list
+ of components (e.g. product price, tax, discount, delivery cost, delivery tax,
+ bonus, etc.).
+ max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the
+ smallest units of the currency (integer, not float/double). For example, for a
+ maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the exp parameter in
+ `currencies.json `_, it
+ shows the number of digits past the decimal point for each currency (2 for the
+ majority of currencies). Defaults to ``0``.
+
+ .. versionadded:: 13.5
+ suggested_tip_amounts (List[:obj:`int`], optional): A JSON-serialized array of
+ suggested amounts of tips in the smallest units of the currency (integer, not
+ float/double). At most 4 suggested tip amounts can be specified. The suggested tip
+ amounts must be positive, passed in a strictly increased order and must not exceed
+ ``max_tip_amount``.
+
+ .. versionadded:: 13.5
+ start_parameter (:obj:`str`, optional): Unique deep-linking parameter. If left empty,
+ *forwarded copies* of the sent message will have a *Pay* button, allowing
+ multiple users to pay directly from the forwarded message, using the same invoice.
+ If non-empty, forwarded copies of the sent message will have a *URL* button with a
+ deep link to the bot (instead of a *Pay* button), with the value used as the
+ start parameter.
+
+ .. versionchanged:: 13.5
+ As of Bot API 5.2, this parameter is optional.
+ provider_data (:obj:`str` | :obj:`object`, optional): JSON-serialized data about the
invoice, which will be shared with the payment provider. A detailed description of
required fields should be provided by the payment provider. When an object is
passed, it will be encoded as JSON.
@@ -2497,53 +3673,64 @@ def send_invoice(self,
photo_size (:obj:`str`, optional): Photo size.
photo_width (:obj:`int`, optional): Photo width.
photo_height (:obj:`int`, optional): Photo height.
- need_name (:obj:`bool`, optional): Pass True, if you require the user's full name to
- complete the order.
- need_phone_number (:obj:`bool`, optional): Pass True, if you require the user's
+ need_name (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's full
+ name to complete the order.
+ need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's
phone number to complete the order.
- need_email (:obj:`bool`, optional): Pass True, if you require the user's email to
- complete the order.
- need_shipping_address (:obj:`bool`, optional): Pass True, if you require the user's
- shipping address to complete the order.
- send_phone_number_to_provider (:obj:`bool`, optional): Pass True, if user's phone
- number should be sent to provider.
- send_email_to_provider (:obj:`bool`, optional): Pass True, if user's email address
- should be sent to provider.
- is_flexible (:obj:`bool`, optional): Pass True, if the final price depends on the
- shipping method.
+ need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email
+ to complete the order.
+ need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the
+ user's shipping address to complete the order.
+ send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's
+ phone number should be sent to provider.
+ send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email
+ address should be sent to provider.
+ is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on
+ the shipping method.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
- reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options.
- An inlinekeyboard. If empty, one 'Pay total price' button will be shown.
- If not empty, the first button must be a Pay button.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized
+ object for an inline keyboard. If empty, one 'Pay total price' button will be
+ shown. If not empty, the first button must be a Pay button.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/sendInvoice'.format(self.base_url)
-
- data = {
+ data: JSONDict = {
'chat_id': chat_id,
'title': title,
'description': description,
'payload': payload,
'provider_token': provider_token,
- 'start_parameter': start_parameter,
'currency': currency,
- 'prices': [p.to_dict() for p in prices]
+ 'prices': [p.to_dict() for p in prices],
}
+ if max_tip_amount is not None:
+ data['max_tip_amount'] = max_tip_amount
+ if suggested_tip_amounts is not None:
+ data['suggested_tip_amounts'] = suggested_tip_amounts
+ if start_parameter is not None:
+ data['start_parameter'] = start_parameter
if provider_data is not None:
- if isinstance(provider_data, string_types):
+ if isinstance(provider_data, str):
data['provider_data'] = provider_data
else:
data['provider_data'] = json.dumps(provider_data)
@@ -2566,46 +3753,60 @@ def send_invoice(self,
if is_flexible is not None:
data['is_flexible'] = is_flexible
if send_phone_number_to_provider is not None:
- data['send_phone_number_to_provider'] = send_email_to_provider
+ data['send_phone_number_to_provider'] = send_phone_number_to_provider
if send_email_to_provider is not None:
data['send_email_to_provider'] = send_email_to_provider
- return url, data
+ return self._message( # type: ignore[return-value]
+ 'sendInvoice',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
@log
- def answer_shipping_query(self,
- shipping_query_id,
- ok,
- shipping_options=None,
- error_message=None,
- timeout=None,
- **kwargs):
+ def answer_shipping_query( # pylint: disable=C0103
+ self,
+ shipping_query_id: str,
+ ok: bool,
+ shipping_options: List[ShippingOption] = None,
+ error_message: str = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""
- If you sent an invoice requesting a shipping address and the parameter is_flexible was
- specified, the Bot API will send an Update with a shipping_query field to the bot. Use
- this method to reply to shipping queries.
+ If you sent an invoice requesting a shipping address and the parameter ``is_flexible`` was
+ specified, the Bot API will send an :class:`telegram.Update` with a
+ :attr:`Update.shipping_query` field to the bot. Use this method to reply to shipping
+ queries.
Args:
shipping_query_id (:obj:`str`): Unique identifier for the query to be answered.
- ok (:obj:`bool`): Specify True if delivery to the specified address is possible and
- False if there are any problems (for example, if delivery to the specified address
- is not possible).
+ ok (:obj:`bool`): Specify :obj:`True` if delivery to the specified address is possible
+ and :obj:`False` if there are any problems (for example, if delivery to the
+ specified address is not possible).
shipping_options (List[:class:`telegram.ShippingOption`]), optional]: Required if ok is
- True. A JSON-serialized array of available shipping options.
- error_message (:obj:`str`, optional): Required if ok is False. Error message in
+ :obj:`True`. A JSON-serialized array of available shipping options.
+ error_message (:obj:`str`, optional): Required if ok is :obj:`False`. Error message in
human readable form that explains why it is impossible to complete the order (e.g.
"Sorry, delivery to your desired address is unavailable"). Telegram will display
this message to the user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`; On success, True is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
ok = bool(ok)
@@ -2613,34 +3814,43 @@ def answer_shipping_query(self,
if ok and (shipping_options is None or error_message is not None):
raise TelegramError(
'answerShippingQuery: If ok is True, shipping_options '
- 'should not be empty and there should not be error_message')
+ 'should not be empty and there should not be error_message'
+ )
if not ok and (shipping_options is not None or error_message is None):
raise TelegramError(
'answerShippingQuery: If ok is False, error_message '
- 'should not be empty and there should not be shipping_options')
+ 'should not be empty and there should not be shipping_options'
+ )
- url_ = '{0}/answerShippingQuery'.format(self.base_url)
-
- data = {'shipping_query_id': shipping_query_id, 'ok': ok}
+ data: JSONDict = {'shipping_query_id': shipping_query_id, 'ok': ok}
if ok:
+ if not shipping_options:
+ # not using an assert statement directly here since they are removed in
+ # the optimized bytecode
+ raise AssertionError
data['shipping_options'] = [option.to_dict() for option in shipping_options]
if error_message is not None:
data['error_message'] = error_message
- data.update(kwargs)
- result = self._request.post(url_, data, timeout=timeout)
+ result = self._post('answerShippingQuery', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def answer_pre_checkout_query(self, pre_checkout_query_id, ok,
- error_message=None, timeout=None, **kwargs):
+ def answer_pre_checkout_query( # pylint: disable=C0103
+ self,
+ pre_checkout_query_id: str,
+ ok: bool,
+ error_message: str = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""
Once the user has confirmed their payment and shipping details, the Bot API sends the final
- confirmation in the form of an Update with the field pre_checkout_query. Use this method to
- respond to such pre-checkout queries.
+ confirmation in the form of an :class:`telegram.Update` with the field
+ :attr:`Update.pre_checkout_query`. Use this method to respond to such pre-checkout queries.
Note:
The Bot API must receive an answer within 10 seconds after the pre-checkout
@@ -2648,53 +3858,64 @@ def answer_pre_checkout_query(self, pre_checkout_query_id, ok,
Args:
pre_checkout_query_id (:obj:`str`): Unique identifier for the query to be answered.
- ok (:obj:`bool`): Specify True if everything is alright (goods are available, etc.) and
- the bot is ready to proceed with the order. Use False if there are any problems.
- error_message (:obj:`str`, optional): Required if ok is False. Error message in human
- readable form that explains the reason for failure to proceed with the checkout
- (e.g. "Sorry, somebody just bought the last of our amazing black T-shirts while you
- were busy filling out your payment details. Please choose a different color or
- garment!"). Telegram will display this message to the user.
+ ok (:obj:`bool`): Specify :obj:`True` if everything is alright
+ (goods are available, etc.) and the bot is ready to proceed with the order. Use
+ :obj:`False` if there are any problems.
+ error_message (:obj:`str`, optional): Required if ok is :obj:`False`. Error message
+ in human readable form that explains the reason for failure to proceed with
+ the checkout (e.g. "Sorry, somebody just bought the last of our amazing black
+ T-shirts while you were busy filling out your payment details. Please choose a
+ different color or garment!"). Telegram will display this message to the user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
ok = bool(ok)
- if not (ok ^ (error_message is not None)):
+ if not (ok ^ (error_message is not None)): # pylint: disable=C0325
raise TelegramError(
'answerPreCheckoutQuery: If ok is True, there should '
'not be error_message; if ok is False, error_message '
- 'should not be empty')
+ 'should not be empty'
+ )
- url_ = '{0}/answerPreCheckoutQuery'.format(self.base_url)
-
- data = {'pre_checkout_query_id': pre_checkout_query_id, 'ok': ok}
+ data: JSONDict = {'pre_checkout_query_id': pre_checkout_query_id, 'ok': ok}
if error_message is not None:
data['error_message'] = error_message
- data.update(kwargs)
- result = self._request.post(url_, data, timeout=timeout)
+ result = self._post('answerPreCheckoutQuery', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def restrict_chat_member(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, timeout=None, **kwargs):
+ def restrict_chat_member(
+ self,
+ chat_id: Union[str, int],
+ user_id: Union[str, int],
+ permissions: ChatPermissions,
+ until_date: Union[int, datetime] = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""
Use this method to restrict a user in a supergroup. The bot must be an administrator in
- the supergroup for this to work and must have the appropriate admin rights. Pass True for
- all boolean parameters to lift restrictions from a user.
+ the supergroup for this to work and must have the appropriate admin rights. Pass
+ :obj:`True` for all boolean parameters to lift restrictions from a user.
+
+ Note:
+ Since Bot API 4.4, :meth:`restrict_chat_member` takes the new user permissions in a
+ single argument of type :class:`telegram.ChatPermissions`. The old way of passing
+ parameters will not keep working forever.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
@@ -2704,98 +3925,116 @@ def restrict_chat_member(self, chat_id, user_id, until_date=None, can_send_messa
will be lifted for the user, unix time. If user is restricted for more than 366
days or less than 30 seconds from the current time, they are considered to be
restricted forever.
- can_send_messages (:obj:`bool`, optional): Pass True, if the user can send text
- messages, contacts, locations and venues.
- can_send_media_messages (:obj:`bool`, optional): Pass True, if the user can send
- audios, documents, photos, videos, video notes and voice notes, implies
- can_send_messages.
- can_send_other_messages (:obj:`bool`, optional): Pass True, if the user can send
- animations, games, stickers and use inline bots, implies can_send_media_messages.
- can_add_web_page_previews (:obj:`bool`, optional): Pass True, if the user may add
- web page previews to their messages, implies can_send_media_messages.
+ For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
+ bot will be used.
+ permissions (:class:`telegram.ChatPermissions`): A JSON-serialized object for new user
+ permissions.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: Returns True on success.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
-
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/restrictChatMember'.format(self.base_url)
-
- data = {'chat_id': chat_id, 'user_id': user_id}
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'user_id': user_id,
+ 'permissions': permissions.to_dict(),
+ }
if until_date is not None:
if isinstance(until_date, datetime):
- until_date = to_timestamp(until_date)
+ until_date = to_timestamp(
+ until_date, tzinfo=self.defaults.tzinfo if self.defaults else None
+ )
data['until_date'] = until_date
- if can_send_messages is not None:
- data['can_send_messages'] = can_send_messages
- if can_send_media_messages is not None:
- data['can_send_media_messages'] = can_send_media_messages
- if can_send_other_messages is not None:
- data['can_send_other_messages'] = can_send_other_messages
- if can_add_web_page_previews is not None:
- data['can_add_web_page_previews'] = can_add_web_page_previews
- data.update(kwargs)
-
- result = self._request.post(url, data, timeout=timeout)
- return result
+ result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
@log
- def promote_chat_member(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, timeout=None, **kwargs):
+ def promote_chat_member(
+ self,
+ chat_id: Union[str, int],
+ user_id: Union[str, int],
+ can_change_info: bool = None,
+ can_post_messages: bool = None,
+ can_edit_messages: bool = None,
+ can_delete_messages: bool = None,
+ can_invite_users: bool = None,
+ can_restrict_members: bool = None,
+ can_pin_messages: bool = None,
+ can_promote_members: bool = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ is_anonymous: bool = None,
+ can_manage_chat: bool = None,
+ can_manage_voice_chats: bool = None,
+ ) -> bool:
"""
Use this method to promote or demote a user in a supergroup or a channel. The bot must be
an administrator in the chat for this to work and must have the appropriate admin rights.
- Pass False for all boolean parameters to demote a user
+ Pass :obj:`False` for all boolean parameters to demote a user.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target supergroup (in the format @supergroupusername).
+ of the target channel (in the format ``@channelusername``).
user_id (:obj:`int`): Unique identifier of the target user.
- can_change_info (:obj:`bool`, optional): Pass True, if the administrator can change
- chat title, photo and other settings.
- can_post_messages (:obj:`bool`, optional): Pass True, if the administrator can
+ is_anonymous (:obj:`bool`, optional): Pass :obj:`True`, if the administrator's presence
+ in the chat is hidden.
+ can_manage_chat (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
+ access the chat event log, chat statistics, message statistics in channels, see
+ channel members, see anonymous administrators in supergroups and ignore slow mode.
+ Implied by any other administrator privilege.
+
+ .. versionadded:: 13.4
+
+ can_manage_voice_chats (:obj:`bool`, optional): Pass :obj:`True`, if the administrator
+ can manage voice chats.
+
+ .. versionadded:: 13.4
+
+ can_change_info (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
+ change chat title, photo and other settings.
+ can_post_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
create channel posts, channels only.
- can_edit_messages (:obj:`bool`, optional): Pass True, if the administrator can edit
- messages of other users, channels only.
- can_delete_messages (:obj:`bool`, optional): Pass True, if the administrator can
+ can_edit_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
+ edit messages of other users and can pin messages, channels only.
+ can_delete_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
delete messages of other users.
- can_invite_users (:obj:`bool`, optional): Pass True, if the administrator can invite
- new users to the chat.
- can_restrict_members (:obj:`bool`, optional): Pass True, if the administrator can
- restrict, ban or unban chat members.
- can_pin_messages (:obj:`bool`, optional): Pass True, if the administrator can pin
- messages, supergroups only.
- can_promote_members (:obj:`bool`, optional): Pass True, if the administrator can add
- new administrators with a subset of his own privileges or demote administrators
+ can_invite_users (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
+ invite new users to the chat.
+ can_restrict_members (:obj:`bool`, optional): Pass :obj:`True`, if the administrator
+ can restrict, ban or unban chat members.
+ can_pin_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
+ pin messages, supergroups only.
+ can_promote_members (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
+ add new administrators with a subset of his own privileges or demote administrators
that he has promoted, directly or indirectly (promoted by administrators that were
appointed by him).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: Returns True on success.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/promoteChatMember'.format(self.base_url)
-
- data = {'chat_id': chat_id, 'user_id': user_id}
+ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id}
+ if is_anonymous is not None:
+ data['is_anonymous'] = is_anonymous
if can_change_info is not None:
data['can_change_info'] = can_change_info
if can_post_messages is not None:
@@ -2812,283 +4051,710 @@ def promote_chat_member(self, chat_id, user_id, can_change_info=None,
data['can_pin_messages'] = can_pin_messages
if can_promote_members is not None:
data['can_promote_members'] = can_promote_members
- data.update(kwargs)
+ if can_manage_chat is not None:
+ data['can_manage_chat'] = can_manage_chat
+ if can_manage_voice_chats is not None:
+ data['can_manage_voice_chats'] = can_manage_voice_chats
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('promoteChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def export_chat_invite_link(self, chat_id, timeout=None, **kwargs):
+ def set_chat_permissions(
+ self,
+ chat_id: Union[str, int],
+ permissions: ChatPermissions,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""
- Use this method to export an invite link to a supergroup or a channel. The bot must be an
- administrator in the chat for this to work and must have the appropriate admin rights.
+ Use this method to set default chat permissions for all members. The bot must be an
+ administrator in the group or a supergroup for this to work and must have the
+ :attr:`telegram.ChatMember.can_restrict_members` admin rights.
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of
+ the target supergroup (in the format `@supergroupusername`).
+ permissions (:class:`telegram.ChatPermissions`): New default chat permissions.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`str`: Exported invite link on success.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/exportChatInviteLink'.format(self.base_url)
-
- data = {'chat_id': chat_id}
- data.update(kwargs)
+ data: JSONDict = {'chat_id': chat_id, 'permissions': permissions.to_dict()}
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('setChatPermissions', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def set_chat_photo(self, chat_id, photo, timeout=None, **kwargs):
- """Use this method to set a new profile photo for the chat.
-
- Photos can't be changed for private chats. The bot must be an administrator in the chat
- for this to work and must have the appropriate admin rights.
+ def set_chat_administrator_custom_title(
+ self,
+ chat_id: Union[int, str],
+ user_id: Union[int, str],
+ custom_title: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to set a custom title for administrators promoted by the bot in a
+ supergroup. The bot must be an administrator for this to work.
Args:
- chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
- photo (`filelike object`): New chat photo.
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of
+ the target supergroup (in the format `@supergroupusername`).
+ user_id (:obj:`int`): Unique identifier of the target administrator.
+ custom_title (:obj:`str`): New custom title for the administrator; 0-16 characters,
+ emoji are not allowed.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments
-
- Note:
- In regular groups (non-supergroups), this method will only work if the
- 'All Members Are Admins' setting is off in the target group.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: Returns True on success.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/setChatPhoto'.format(self.base_url)
-
- if InputFile.is_file(photo):
- photo = InputFile(photo)
+ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id, 'custom_title': custom_title}
- data = {'chat_id': chat_id, 'photo': photo}
- data.update(kwargs)
+ result = self._post(
+ 'setChatAdministratorCustomTitle', data, timeout=timeout, api_kwargs=api_kwargs
+ )
- result = self._request.post(url, data, timeout=timeout)
-
- return result
+ return result # type: ignore[return-value]
@log
- def delete_chat_photo(self, chat_id, timeout=None, **kwargs):
+ def export_chat_invite_link(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> str:
"""
- Use this method to delete a chat photo. Photos can't be changed for private chats. The bot
- must be an administrator in the chat for this to work and must have the appropriate admin
- rights.
+ Use this method to generate a new primary invite link for a chat; any previously generated
+ link is revoked. The bot must be an administrator in the chat for this to work and must
+ have the appropriate admin rights.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ of the target channel (in the format ``@channelusername``).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Note:
- In regular groups (non-supergroups), this method will only work if the
- 'All Members Are Admins' setting is off in the target group.
+ Each administrator in a chat generates their own invite links. Bots can't use invite
+ links generated by other administrators. If you want your bot to work with invite
+ links, it will need to generate its own link using :meth:`export_chat_invite_link` or
+ by calling the :meth:`get_chat` method. If your bot needs to generate a new primary
+ invite link replacing its previous one, use :attr:`export_chat_invite_link` again.
Returns:
- :obj:`bool`: Returns ``True`` on success.
+ :obj:`str`: New invite link on success.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/deleteChatPhoto'.format(self.base_url)
-
- data = {'chat_id': chat_id}
- data.update(kwargs)
+ data: JSONDict = {'chat_id': chat_id}
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('exportChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def set_chat_title(self, chat_id, title, timeout=None, **kwargs):
+ def create_chat_invite_link(
+ self,
+ chat_id: Union[str, int],
+ expire_date: Union[int, datetime] = None,
+ member_limit: int = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ name: str = None,
+ creates_join_request: bool = None,
+ ) -> ChatInviteLink:
"""
- Use this method to change the title of a chat. Titles can't be changed for private chats.
- The bot must be an administrator in the chat for this to work and must have the appropriate
- admin rights.
+ Use this method to create an additional invite link for a chat. The bot must be an
+ administrator in the chat for this to work and must have the appropriate admin rights.
+ The link can be revoked using the method :meth:`revoke_chat_invite_link`.
+
+ .. versionadded:: 13.4
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
- title (:obj:`str`): New chat title, 1-255 characters.
+ of the target channel (in the format ``@channelusername``).
+ expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will
+ expire. Integer input will be interpreted as Unix timestamp.
+ For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
+ bot will be used.
+ member_limit (:obj:`int`, optional): Maximum number of users that can be members of
+ the chat simultaneously after joining the chat via this invite link; 1-99999.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+ name (:obj:`str`, optional): Invite link name; 0-32 characters.
- Note:
- In regular groups (non-supergroups), this method will only work if the
- 'All Members Are Admins' setting is off in the target group.
+ .. versionadded:: 13.8
+ creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat
+ via the link need to be approved by chat administrators.
+ If :obj:`True`, ``member_limit`` can't be specified.
+
+ .. versionadded:: 13.8
Returns:
- :obj:`bool`: Returns ``True`` on success.
+ :class:`telegram.ChatInviteLink`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/setChatTitle'.format(self.base_url)
+ if creates_join_request and member_limit:
+ raise ValueError(
+ "If `creates_join_request` is `True`, `member_limit` can't be specified."
+ )
- data = {'chat_id': chat_id, 'title': title}
- data.update(kwargs)
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ }
- result = self._request.post(url, data, timeout=timeout)
+ if expire_date is not None:
+ if isinstance(expire_date, datetime):
+ expire_date = to_timestamp(
+ expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None
+ )
+ data['expire_date'] = expire_date
- return result
+ if member_limit is not None:
+ data['member_limit'] = member_limit
+
+ if name is not None:
+ data['name'] = name
+
+ if creates_join_request is not None:
+ data['creates_join_request'] = creates_join_request
+
+ result = self._post('createChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type]
@log
- def set_chat_description(self, chat_id, description, timeout=None, **kwargs):
+ def edit_chat_invite_link(
+ self,
+ chat_id: Union[str, int],
+ invite_link: str,
+ expire_date: Union[int, datetime] = None,
+ member_limit: int = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ name: str = None,
+ creates_join_request: bool = None,
+ ) -> ChatInviteLink:
"""
- Use this method to change the description of a supergroup or a channel. The bot must be an
+ Use this method to edit a non-primary invite link created by the bot. The bot must be an
administrator in the chat for this to work and must have the appropriate admin rights.
+ Note:
+ Though not stated explicitly in the official docs, Telegram changes not only the
+ optional parameters that are explicitly passed, but also replaces all other optional
+ parameters to the default values. However, since not documented, this behaviour may
+ change unbeknown to PTB.
+
+ .. versionadded:: 13.4
+
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
- description (:obj:`str`): New chat description, 1-255 characters.
+ of the target channel (in the format ``@channelusername``).
+ invite_link (:obj:`str`): The invite link to edit.
+ expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will
+ expire.
+ For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
+ bot will be used.
+ member_limit (:obj:`int`, optional): Maximum number of users that can be members of
+ the chat simultaneously after joining the chat via this invite link; 1-99999.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+ name (:obj:`str`, optional): Invite link name; 0-32 characters.
+
+ .. versionadded:: 13.8
+ creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat
+ via the link need to be approved by chat administrators.
+ If :obj:`True`, ``member_limit`` can't be specified.
+
+ .. versionadded:: 13.8
Returns:
- :obj:`bool`: Returns ``True`` on success.
+ :class:`telegram.ChatInviteLink`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/setChatDescription'.format(self.base_url)
+ if creates_join_request and member_limit:
+ raise ValueError(
+ "If `creates_join_request` is `True`, `member_limit` can't be specified."
+ )
- data = {'chat_id': chat_id, 'description': description}
- data.update(kwargs)
+ data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link}
- result = self._request.post(url, data, timeout=timeout)
+ if expire_date is not None:
+ if isinstance(expire_date, datetime):
+ expire_date = to_timestamp(
+ expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None
+ )
+ data['expire_date'] = expire_date
- return result
+ if member_limit is not None:
+ data['member_limit'] = member_limit
+
+ if name is not None:
+ data['name'] = name
+
+ if creates_join_request is not None:
+ data['creates_join_request'] = creates_join_request
+
+ result = self._post('editChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type]
@log
- def pin_chat_message(self, chat_id, message_id, disable_notification=None, timeout=None,
- **kwargs):
+ def revoke_chat_invite_link(
+ self,
+ chat_id: Union[str, int],
+ invite_link: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> ChatInviteLink:
"""
- Use this method to pin a message in a supergroup. The bot must be an administrator in the
+ Use this method to revoke an invite link created by the bot. If the primary link is
+ revoked, a new link is automatically generated. The bot must be an administrator in the
chat for this to work and must have the appropriate admin rights.
+ .. versionadded:: 13.4
+
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
- message_id (:obj:`int`): Identifier of a message to pin.
- disable_notification (:obj:`bool`, optional): Pass True, if it is not necessary to send
- a notification to all group members about the new pinned message.
+ of the target channel (in the format ``@channelusername``).
+ invite_link (:obj:`str`): The invite link to edit.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: Returns ``True`` on success.
+ :class:`telegram.ChatInviteLink`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/pinChatMessage'.format(self.base_url)
+ data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link}
- data = {'chat_id': chat_id, 'message_id': message_id}
+ result = self._post('revokeChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs)
- if disable_notification is not None:
- data['disable_notification'] = disable_notification
- data.update(kwargs)
+ return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type]
- result = self._request.post(url, data, timeout=timeout)
+ @log
+ def approve_chat_join_request(
+ self,
+ chat_id: Union[str, int],
+ user_id: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Use this method to approve a chat join request.
- return result
+ The bot must be an administrator in the chat for this to work and must have the
+ :attr:`telegram.ChatPermissions.can_invite_users` administrator right.
- @log
- def unpin_chat_message(self, chat_id, timeout=None, **kwargs):
- """
- Use this method to unpin a message in a supergroup. The bot must be an administrator in the
- chat for this to work and must have the appropriate admin rights.
+ .. versionadded:: 13.8
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
- of the target`channel (in the format @channelusername).
+ of the target channel (in the format ``@channelusername``).
+ user_id (:obj:`int`): Unique identifier of the target user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: Returns ``True`` on success.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
-
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/unpinChatMessage'.format(self.base_url)
-
- data = {'chat_id': chat_id}
- data.update(kwargs)
+ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id}
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('approveChatJoinRequest', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def get_sticker_set(self, name, timeout=None, **kwargs):
- """Use this method to get a sticker set.
+ def decline_chat_join_request(
+ self,
+ chat_id: Union[str, int],
+ user_id: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Use this method to decline a chat join request.
- Args:
- name (:obj:`str`): Short name of the sticker set that is used in t.me/addstickers/
- URLs (e.g., animals)
- timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout from the server (instead of the one specified during
- creation of the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ The bot must be an administrator in the chat for this to work and must have the
+ :attr:`telegram.ChatPermissions.can_invite_users` administrator right.
- Returns:
- :class:`telegram.StickerSet`
+ .. versionadded:: 13.8
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ user_id (:obj:`int`): Unique identifier of the target user.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id}
+
+ result = self._post('declineChatJoinRequest', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
+
+ @log
+ def set_chat_photo(
+ self,
+ chat_id: Union[str, int],
+ photo: FileInput,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Use this method to set a new profile photo for the chat.
+
+ Photos can't be changed for private chats. The bot must be an administrator in the chat
+ for this to work and must have the appropriate admin rights.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ photo (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`): New chat photo.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'chat_id': chat_id, 'photo': parse_file_input(photo)}
+
+ result = self._post('setChatPhoto', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
+
+ @log
+ def delete_chat_photo(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to delete a chat photo. Photos can't be changed for private chats. The bot
+ must be an administrator in the chat for this to work and must have the appropriate admin
+ rights.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'chat_id': chat_id}
+
+ result = self._post('deleteChatPhoto', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
+
+ @log
+ def set_chat_title(
+ self,
+ chat_id: Union[str, int],
+ title: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to change the title of a chat. Titles can't be changed for private chats.
+ The bot must be an administrator in the chat for this to work and must have the appropriate
+ admin rights.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ title (:obj:`str`): New chat title, 1-255 characters.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'chat_id': chat_id, 'title': title}
+
+ result = self._post('setChatTitle', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
+
+ @log
+ def set_chat_description(
+ self,
+ chat_id: Union[str, int],
+ description: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to change the description of a group, a supergroup or a channel. The bot
+ must be an administrator in the chat for this to work and must have the appropriate admin
+ rights.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ description (:obj:`str`): New chat description, 0-255 characters.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'chat_id': chat_id, 'description': description}
+
+ result = self._post('setChatDescription', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
+
+ @log
+ def pin_chat_message(
+ self,
+ chat_id: Union[str, int],
+ message_id: int,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to add a message to the list of pinned messages in a chat. If the
+ chat is not a private chat, the bot must be an administrator in the chat for this to work
+ and must have the :attr:`telegram.ChatMember.can_pin_messages` admin right in a supergroup
+ or :attr:`telegram.ChatMember.can_edit_messages` admin right in a channel.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ message_id (:obj:`int`): Identifier of a message to pin.
+ disable_notification (:obj:`bool`, optional): Pass :obj:`True`, if it is not necessary
+ to send a notification to all chat members about the new pinned message.
+ Notifications are always disabled in channels and private chats.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'message_id': message_id,
+ 'disable_notification': disable_notification,
+ }
+
+ return self._post( # type: ignore[return-value]
+ 'pinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ @log
+ def unpin_chat_message(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ message_id: int = None,
+ ) -> bool:
+ """
+ Use this method to remove a message from the list of pinned messages in a chat. If the
+ chat is not a private chat, the bot must be an administrator in the chat for this to work
+ and must have the :attr:`telegram.ChatMember.can_pin_messages` admin right in a
+ supergroup or :attr:`telegram.ChatMember.can_edit_messages` admin right in a channel.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ message_id (:obj:`int`, optional): Identifier of a message to unpin. If not specified,
+ the most recent pinned message (by sending date) will be unpinned.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'chat_id': chat_id}
+
+ if message_id is not None:
+ data['message_id'] = message_id
+ return self._post( # type: ignore[return-value]
+ 'unpinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ @log
+ def unpin_all_chat_messages(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""
- url = '{0}/getStickerSet'.format(self.base_url)
+ Use this method to clear the list of pinned messages in a chat. If the
+ chat is not a private chat, the bot must be an administrator in the chat for this
+ to work and must have the :attr:`telegram.ChatMember.can_pin_messages` admin right in a
+ supergroup or :attr:`telegram.ChatMember.can_edit_messages` admin right in a channel.
- data = {'name': name}
- data.update(kwargs)
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
- result = self._request.post(url, data, timeout=timeout)
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
- return StickerSet.de_json(result, self)
+ """
+ data: JSONDict = {'chat_id': chat_id}
+
+ return self._post( # type: ignore[return-value]
+ 'unpinAllChatMessages', data, timeout=timeout, api_kwargs=api_kwargs
+ )
@log
- def upload_sticker_file(self, user_id, png_sticker, timeout=None, **kwargs):
+ def get_sticker_set(
+ self,
+ name: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> StickerSet:
+ """Use this method to get a sticker set.
+
+ Args:
+ name (:obj:`str`): Name of the sticker set.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during
+ creation of the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :class:`telegram.StickerSet`
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
"""
- Use this method to upload a .png file with a sticker for later use in
- :attr:`create_new_sticker_set` and :attr:`add_sticker_to_set` methods (can be used multiple
+ data: JSONDict = {'name': name}
+
+ result = self._post('getStickerSet', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return StickerSet.de_json(result, self) # type: ignore[return-value, arg-type]
+
+ @log
+ def upload_sticker_file(
+ self,
+ user_id: Union[str, int],
+ png_sticker: FileInput,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ ) -> File:
+ """
+ Use this method to upload a ``.PNG`` file with a sticker for later use in
+ :meth:`create_new_sticker_set` and :meth:`add_sticker_to_set` methods (can be used multiple
times).
Note:
@@ -3097,43 +4763,60 @@ def upload_sticker_file(self, user_id, png_sticker, timeout=None, **kwargs):
Args:
user_id (:obj:`int`): User identifier of sticker file owner.
- png_sticker (:obj:`str` | `filelike object`): Png image with the sticker,
- must be up to 512 kilobytes in size, dimensions must not exceed 512px,
- and either width or height must be exactly 512px.
+ png_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`):
+ **PNG** image with the sticker, must be up to 512 kilobytes in size,
+ dimensions must not exceed 512px, and either width or height must be exactly 512px.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during
creation of the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :class:`telegram.File`: The uploaded File
+ :class:`telegram.File`: On success, the uploaded File is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/uploadStickerFile'.format(self.base_url)
-
- if InputFile.is_file(png_sticker):
- png_sticker = InputFile(png_sticker)
-
- data = {'user_id': user_id, 'png_sticker': png_sticker}
- data.update(kwargs)
+ data: JSONDict = {'user_id': user_id, 'png_sticker': parse_file_input(png_sticker)}
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('uploadStickerFile', data, timeout=timeout, api_kwargs=api_kwargs)
- return File.de_json(result, self)
+ return File.de_json(result, self) # type: ignore[return-value, arg-type]
@log
- def create_new_sticker_set(self, user_id, name, title, png_sticker, emojis,
- contains_masks=None, mask_position=None, timeout=None, **kwargs):
- """Use this method to create new sticker set owned by a user.
-
+ def create_new_sticker_set(
+ self,
+ user_id: Union[str, int],
+ name: str,
+ title: str,
+ emojis: str,
+ png_sticker: FileInput = None,
+ contains_masks: bool = None,
+ mask_position: MaskPosition = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ tgs_sticker: FileInput = None,
+ api_kwargs: JSONDict = None,
+ webm_sticker: FileInput = None,
+ ) -> bool:
+ """
+ Use this method to create new sticker set owned by a user.
The bot will be able to edit the created sticker set.
+ You must use exactly one of the fields ``png_sticker``, ``tgs_sticker``, or
+ ``webm_sticker``.
+
+ Warning:
+ As of API 4.7 ``png_sticker`` is an optional argument and therefore the order of the
+ arguments had to be changed. Use keyword arguments to make sure that the arguments are
+ passed correctly.
Note:
- The png_sticker argument can be either a file_id, an URL or a file from disk
- ``open(filename, 'rb')``
+ The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from
+ disk ``open(filename, 'rb')``
Args:
user_id (:obj:`int`): User identifier of created sticker set owner.
@@ -3143,97 +4826,164 @@ def create_new_sticker_set(self, user_id, name, title, png_sticker, emojis,
must end in "_by_". is case insensitive.
1-64 characters.
title (:obj:`str`): Sticker set title, 1-64 characters.
- png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, must be up
- to 512 kilobytes in size, dimensions must not exceed 512px,
+ png_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \
+ optional): **PNG** image with the sticker,
+ must be up to 512 kilobytes in size, dimensions must not exceed 512px,
and either width or height must be exactly 512px. Pass a file_id as a String to
send a file that already exists on the Telegram servers, pass an HTTP URL as a
String for Telegram to get a file from the Internet, or upload a new one
using multipart/form-data.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ tgs_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \
+ optional): **TGS** animation with the sticker, uploaded using multipart/form-data.
+ See https://core.telegram.org/stickers#animated-sticker-requirements for technical
+ requirements.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\
+ optional): **WEBM** video with the sticker, uploaded using multipart/form-data.
+ See https://core.telegram.org/stickers#video-sticker-requirements for
+ technical requirements.
+
+ .. versionadded:: 13.11
+
emojis (:obj:`str`): One or more emoji corresponding to the sticker.
- contains_masks (:obj:`bool`, optional): Pass True, if a set of mask stickers should be
- created.
+ contains_masks (:obj:`bool`, optional): Pass :obj:`True`, if a set of mask stickers
+ should be created.
mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask
should be placed on faces.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during
creation of the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/createNewStickerSet'.format(self.base_url)
-
- if InputFile.is_file(png_sticker):
- png_sticker = InputFile(png_sticker)
-
- data = {'user_id': user_id, 'name': name, 'title': title, 'png_sticker': png_sticker,
- 'emojis': emojis}
-
+ data: JSONDict = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis}
+
+ if png_sticker is not None:
+ data['png_sticker'] = parse_file_input(png_sticker)
+ if tgs_sticker is not None:
+ data['tgs_sticker'] = parse_file_input(tgs_sticker)
+ if webm_sticker is not None:
+ data['webm_sticker'] = parse_file_input(webm_sticker)
if contains_masks is not None:
data['contains_masks'] = contains_masks
if mask_position is not None:
- data['mask_position'] = mask_position
- data.update(kwargs)
+ # We need to_json() instead of to_dict() here, because we're sending a media
+ # message here, which isn't json dumped by utils.request
+ data['mask_position'] = mask_position.to_json()
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('createNewStickerSet', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def add_sticker_to_set(self, user_id, name, png_sticker, emojis, mask_position=None,
- timeout=None, **kwargs):
- """Use this method to add a new sticker to a set created by the bot.
+ def add_sticker_to_set(
+ self,
+ user_id: Union[str, int],
+ name: str,
+ emojis: str,
+ png_sticker: FileInput = None,
+ mask_position: MaskPosition = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ tgs_sticker: FileInput = None,
+ api_kwargs: JSONDict = None,
+ webm_sticker: FileInput = None,
+ ) -> bool:
+ """
+ Use this method to add a new sticker to a set created by the bot.
+ You **must** use exactly one of the fields ``png_sticker``, ``tgs_sticker`` or
+ ``webm_sticker``. Animated stickers can be added to animated sticker sets and only to them.
+ Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120
+ stickers.
+
+ Warning:
+ As of API 4.7 ``png_sticker`` is an optional argument and therefore the order of the
+ arguments had to be changed. Use keyword arguments to make sure that the arguments are
+ passed correctly.
Note:
- The png_sticker argument can be either a file_id, an URL or a file from disk
- ``open(filename, 'rb')``
+ The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from
+ disk ``open(filename, 'rb')``
Args:
user_id (:obj:`int`): User identifier of created sticker set owner.
+
name (:obj:`str`): Sticker set name.
- png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, must be up
- to 512 kilobytes in size, dimensions must not exceed 512px,
+ png_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \
+ optional): **PNG** image with the sticker,
+ must be up to 512 kilobytes in size, dimensions must not exceed 512px,
and either width or height must be exactly 512px. Pass a file_id as a String to
send a file that already exists on the Telegram servers, pass an HTTP URL as a
String for Telegram to get a file from the Internet, or upload a new one
using multipart/form-data.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ tgs_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \
+ optional): **TGS** animation with the sticker, uploaded using multipart/form-data.
+ See https://core.telegram.org/stickers#animated-sticker-requirements for technical
+ requirements.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\
+ optional): **WEBM** video with the sticker, uploaded using multipart/form-data.
+ See https://core.telegram.org/stickers#video-sticker-requirements for
+ technical requirements.
+
+ .. versionadded:: 13.11
emojis (:obj:`str`): One or more emoji corresponding to the sticker.
mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask
- should beplaced on faces.
+ should be placed on faces.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during
creation of the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/addStickerToSet'.format(self.base_url)
-
- if InputFile.is_file(png_sticker):
- png_sticker = InputFile(png_sticker)
-
- data = {'user_id': user_id, 'name': name, 'png_sticker': png_sticker, 'emojis': emojis}
-
+ data: JSONDict = {'user_id': user_id, 'name': name, 'emojis': emojis}
+
+ if png_sticker is not None:
+ data['png_sticker'] = parse_file_input(png_sticker)
+ if tgs_sticker is not None:
+ data['tgs_sticker'] = parse_file_input(tgs_sticker)
+ if webm_sticker is not None:
+ data['webm_sticker'] = parse_file_input(webm_sticker)
if mask_position is not None:
- data['mask_position'] = mask_position
- data.update(kwargs)
+ # We need to_json() instead of to_dict() here, because we're sending a media
+ # message here, which isn't json dumped by utils.request
+ data['mask_position'] = mask_position.to_json()
- result = self._request.post(url, data, timeout=timeout)
+ result = self._post('addStickerToSet', data, timeout=timeout, api_kwargs=api_kwargs)
- return result
+ return result # type: ignore[return-value]
@log
- def set_sticker_position_in_set(self, sticker, position, timeout=None, **kwargs):
+ def set_sticker_position_in_set(
+ self,
+ sticker: str,
+ position: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Use this method to move a sticker in a set created by the bot to a specific position.
Args:
@@ -3242,26 +4992,31 @@ def set_sticker_position_in_set(self, sticker, position, timeout=None, **kwargs)
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during
creation of the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/setStickerPositionInSet'.format(self.base_url)
+ data: JSONDict = {'sticker': sticker, 'position': position}
- data = {'sticker': sticker, 'position': position}
- data.update(kwargs)
+ result = self._post(
+ 'setStickerPositionInSet', data, timeout=timeout, api_kwargs=api_kwargs
+ )
- result = self._request.post(url, data, timeout=timeout)
-
- return result
+ return result # type: ignore[return-value]
@log
- def delete_sticker_from_set(self, sticker, timeout=None, **kwargs):
+ def delete_sticker_from_set(
+ self,
+ sticker: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Use this method to delete a sticker from a set created by the bot.
Args:
@@ -3269,31 +5024,90 @@ def delete_sticker_from_set(self, sticker, timeout=None, **kwargs):
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during
creation of the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url = '{0}/deleteStickerFromSet'.format(self.base_url)
+ data: JSONDict = {'sticker': sticker}
- data = {'sticker': sticker}
- data.update(kwargs)
+ result = self._post('deleteStickerFromSet', data, timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.post(url, data, timeout=timeout)
+ return result # type: ignore[return-value]
- return result
+ @log
+ def set_sticker_set_thumb(
+ self,
+ name: str,
+ user_id: Union[str, int],
+ thumb: FileInput = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set
+ for animated sticker sets only. Video thumbnails can be set only for video sticker sets
+ only.
+
+ Note:
+ The thumb can be either a file_id, an URL or a file from disk ``open(filename, 'rb')``
+
+ Args:
+ name (:obj:`str`): Sticker set name
+ user_id (:obj:`int`): User identifier of created sticker set owner.
+ thumb (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \
+ optional): A **PNG** image with the thumbnail, must
+ be up to 128 kilobytes in size and have width and height exactly 100px, or a
+ **TGS** animation with the thumbnail up to 32 kilobytes in size; see
+ https://core.telegram.org/stickers#animated-sticker-requirements for animated
+ sticker technical requirements, or a **WEBM** video with the thumbnail up to 32
+ kilobytes in size; see
+ https://core.telegram.org/stickers#video-sticker-requirements for video sticker
+ technical requirements. Pass a file_id as a String to send a file that
+ already exists on the Telegram servers, pass an HTTP URL as a String for Telegram
+ to get a file from the Internet, or upload a new one using multipart/form-data.
+ Animated sticker set thumbnails can't be uploaded via HTTP URL.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during
+ creation of the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'name': name, 'user_id': user_id}
+
+ if thumb is not None:
+ data['thumb'] = parse_file_input(thumb)
+
+ result = self._post('setStickerSetThumb', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return result # type: ignore[return-value]
@log
- def set_passport_data_errors(self, user_id, errors, timeout=None, **kwargs):
+ def set_passport_data_errors(
+ self,
+ user_id: Union[str, int],
+ errors: List[PassportElementError],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""
Informs a user that some of the Telegram Passport elements they provided contains errors.
The user will not be able to re-submit their Passport to you until the errors are fixed
- (the contents of the field for which you returned the error must change). Returns True
- on success.
+ (the contents of the field for which you returned the error must change).
Use this if the data submitted by the user doesn't satisfy the standards your service
requires for any reason. For example, if a birthday date seems invalid, a submitted
@@ -3307,158 +5121,741 @@ def set_passport_data_errors(self, user_id, errors, timeout=None, **kwargs):
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during
creation of the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
Returns:
- :obj:`bool`: On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- url_ = '{0}/setPassportDataErrors'.format(self.base_url)
+ data: JSONDict = {'user_id': user_id, 'errors': [error.to_dict() for error in errors]}
- data = {'user_id': user_id, 'errors': [error.to_dict() for error in errors]}
- data.update(kwargs)
+ result = self._post('setPassportDataErrors', data, timeout=timeout, api_kwargs=api_kwargs)
- result = self._request.post(url_, data, timeout=timeout)
+ return result # type: ignore[return-value]
- return result
+ @log
+ def send_poll(
+ self,
+ chat_id: Union[int, str],
+ question: str,
+ options: List[str],
+ is_anonymous: bool = True,
+ type: str = Poll.REGULAR, # pylint: disable=W0622
+ allows_multiple_answers: bool = False,
+ correct_option_id: int = None,
+ is_closed: bool = None,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ explanation: str = None,
+ explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
+ open_period: int = None,
+ close_date: Union[int, datetime] = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ protect_content: bool = None,
+ ) -> Message:
+ """
+ Use this method to send a native poll.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ question (:obj:`str`): Poll question, 1-300 characters.
+ options (List[:obj:`str`]): List of answer options, 2-10 strings 1-100 characters each.
+ is_anonymous (:obj:`bool`, optional): :obj:`True`, if the poll needs to be anonymous,
+ defaults to :obj:`True`.
+ type (:obj:`str`, optional): Poll type, :attr:`telegram.Poll.QUIZ` or
+ :attr:`telegram.Poll.REGULAR`, defaults to :attr:`telegram.Poll.REGULAR`.
+ allows_multiple_answers (:obj:`bool`, optional): :obj:`True`, if the poll allows
+ multiple answers, ignored for polls in quiz mode, defaults to :obj:`False`.
+ correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer
+ option, required for polls in quiz mode.
+ explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect
+ answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most
+ 2 line feeds after entities parsing.
+ explanation_parse_mode (:obj:`str`, optional): Mode for parsing entities in the
+ explanation. See the constants in :class:`telegram.ParseMode` for the available
+ modes.
+ explanation_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in message text, which can be specified instead of
+ :attr:`parse_mode`.
+ open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active
+ after creation, 5-600. Can't be used together with :attr:`close_date`.
+ close_date (:obj:`int` | :obj:`datetime.datetime`, optional): Point in time (Unix
+ timestamp) when the poll will be automatically closed. Must be at least 5 and no
+ more than 600 seconds in the future. Can't be used together with
+ :attr:`open_period`.
+ For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
+ bot will be used.
+ is_closed (:obj:`bool`, optional): Pass :obj:`True`, if the poll needs to be
+ immediately closed. This can be useful for poll preview.
+ disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
+ receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
+ reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
+ original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
+ reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
+ JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
+ to remove reply keyboard or to force a reply from the user.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :class:`telegram.Message`: On success, the sent Message is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'question': question,
+ 'options': options,
+ 'explanation_parse_mode': explanation_parse_mode,
+ }
+
+ if not is_anonymous:
+ data['is_anonymous'] = is_anonymous
+ if type:
+ data['type'] = type
+ if allows_multiple_answers:
+ data['allows_multiple_answers'] = allows_multiple_answers
+ if correct_option_id is not None:
+ data['correct_option_id'] = correct_option_id
+ if is_closed:
+ data['is_closed'] = is_closed
+ if explanation:
+ data['explanation'] = explanation
+ if explanation_entities:
+ data['explanation_entities'] = [me.to_dict() for me in explanation_entities]
+ if open_period:
+ data['open_period'] = open_period
+ if close_date:
+ if isinstance(close_date, datetime):
+ close_date = to_timestamp(
+ close_date, tzinfo=self.defaults.tzinfo if self.defaults else None
+ )
+ data['close_date'] = close_date
+
+ return self._message( # type: ignore[return-value]
+ 'sendPoll',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ @log
+ def stop_poll(
+ self,
+ chat_id: Union[int, str],
+ message_id: int,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Poll:
+ """
+ Use this method to stop a poll which was sent by the bot.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ message_id (:obj:`int`): Identifier of the original message with the poll.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized
+ object for a new message inline keyboard.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :class:`telegram.Poll`: On success, the stopped Poll with the final results is
+ returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'chat_id': chat_id, 'message_id': message_id}
+
+ if reply_markup:
+ if isinstance(reply_markup, ReplyMarkup):
+ # We need to_json() instead of to_dict() here, because reply_markups may be
+ # attached to media messages, which aren't json dumped by utils.request
+ data['reply_markup'] = reply_markup.to_json()
+ else:
+ data['reply_markup'] = reply_markup
+
+ result = self._post('stopPoll', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ return Poll.de_json(result, self) # type: ignore[return-value, arg-type]
+
+ @log
+ def send_dice(
+ self,
+ chat_id: Union[int, str],
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ emoji: str = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> Message:
+ """
+ Use this method to send an animated emoji that will display a random value.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based.
+ Currently, must be one of “🎲”, “🎯”, “🏀”, “⚽”, "🎳", or “🎰”. Dice can have
+ values 1-6 for “🎲”, “🎯” and "🎳", values 1-5 for “🏀” and “⚽”, and values 1-64
+ for “🎰”. Defaults to “🎲”.
+
+ .. versionchanged:: 13.4
+ Added the "🎳" emoji.
+ disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
+ receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
+ reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
+ original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
+ reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
+ JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
+ to remove reply keyboard or to force a reply from the user.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :class:`telegram.Message`: On success, the sent Message is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {'chat_id': chat_id}
+
+ if emoji:
+ data['emoji'] = emoji
+
+ return self._message( # type: ignore[return-value]
+ 'sendDice',
+ data,
+ timeout=timeout,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ allow_sending_without_reply=allow_sending_without_reply,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ @log
+ def get_my_commands(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ scope: BotCommandScope = None,
+ language_code: str = None,
+ ) -> List[BotCommand]:
+ """
+ Use this method to get the current list of the bot's commands for the given scope and user
+ language.
+
+ Args:
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+ scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object,
+ describing scope of users. Defaults to :class:`telegram.BotCommandScopeDefault`.
+
+ .. versionadded:: 13.7
+
+ language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code or an empty
+ string.
+
+ .. versionadded:: 13.7
+
+ Returns:
+ List[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty
+ list is returned if commands are not set.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {}
+
+ if scope:
+ data['scope'] = scope.to_dict()
+
+ if language_code:
+ data['language_code'] = language_code
+
+ result = self._post('getMyCommands', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ if (scope is None or scope.type == scope.DEFAULT) and language_code is None:
+ self._commands = BotCommand.de_list(result, self) # type: ignore[assignment,arg-type]
+ return self._commands # type: ignore[return-value]
+
+ return BotCommand.de_list(result, self) # type: ignore[return-value,arg-type]
+
+ @log
+ def set_my_commands(
+ self,
+ commands: List[Union[BotCommand, Tuple[str, str]]],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ scope: BotCommandScope = None,
+ language_code: str = None,
+ ) -> bool:
+ """
+ Use this method to change the list of the bot's commands. See the
+ `Telegram docs `_ for more details about bot
+ commands.
- def to_dict(self):
- data = {'id': self.id, 'username': self.username, 'first_name': self.username}
+ Args:
+ commands (List[:class:`BotCommand` | (:obj:`str`, :obj:`str`)]): A JSON-serialized list
+ of bot commands to be set as the list of the bot's commands. At most 100 commands
+ can be specified.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+ scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object,
+ describing scope of users for which the commands are relevant. Defaults to
+ :class:`telegram.BotCommandScopeDefault`.
+
+ .. versionadded:: 13.7
+
+ language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty,
+ commands will be applied to all users from the given scope, for whose language
+ there are no dedicated commands.
+
+ .. versionadded:: 13.7
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ cmds = [c if isinstance(c, BotCommand) else BotCommand(c[0], c[1]) for c in commands]
+
+ data: JSONDict = {'commands': [c.to_dict() for c in cmds]}
+
+ if scope:
+ data['scope'] = scope.to_dict()
+
+ if language_code:
+ data['language_code'] = language_code
+
+ result = self._post('setMyCommands', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ # Set commands only for default scope. No need to check for outcome.
+ # If request failed, we won't come this far
+ if (scope is None or scope.type == scope.DEFAULT) and language_code is None:
+ self._commands = cmds
+
+ return result # type: ignore[return-value]
+
+ @log
+ def delete_my_commands(
+ self,
+ scope: BotCommandScope = None,
+ language_code: str = None,
+ api_kwargs: JSONDict = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ ) -> bool:
+ """
+ Use this method to delete the list of the bot's commands for the given scope and user
+ language. After deletion,
+ `higher level commands `_
+ will be shown to affected users.
+
+ .. versionadded:: 13.7
+
+ Args:
+ scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object,
+ describing scope of users for which the commands are relevant. Defaults to
+ :class:`telegram.BotCommandScopeDefault`.
+ language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty,
+ commands will be applied to all users from the given scope, for whose language
+ there are no dedicated commands.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {}
+
+ if scope:
+ data['scope'] = scope.to_dict()
+
+ if language_code:
+ data['language_code'] = language_code
+
+ result = self._post('deleteMyCommands', data, timeout=timeout, api_kwargs=api_kwargs)
+
+ if (scope is None or scope.type == scope.DEFAULT) and language_code is None:
+ self._commands = []
+
+ return result # type: ignore[return-value]
+
+ @log
+ def log_out(self, timeout: ODVInput[float] = DEFAULT_NONE) -> bool:
+ """
+ Use this method to log out from the cloud Bot API server before launching the bot locally.
+ You *must* log out the bot before running it locally, otherwise there is no guarantee that
+ the bot will receive updates. After a successful call, you can immediately log in on a
+ local server, but will not be able to log in back to the cloud Bot API server for 10
+ minutes.
+
+ Args:
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+
+ Returns:
+ :obj:`True`: On success
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ return self._post('logOut', timeout=timeout) # type: ignore[return-value]
+
+ @log
+ def close(self, timeout: ODVInput[float] = DEFAULT_NONE) -> bool:
+ """
+ Use this method to close the bot instance before moving it from one local server to
+ another. You need to delete the webhook before calling this method to ensure that the bot
+ isn't launched again after server restart. The method will return error 429 in the first
+ 10 minutes after the bot is launched.
+
+ Args:
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+
+ Returns:
+ :obj:`True`: On success
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ return self._post('close', timeout=timeout) # type: ignore[return-value]
+
+ @log
+ def copy_message(
+ self,
+ chat_id: Union[int, str],
+ from_chat_id: Union[str, int],
+ message_id: int,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> MessageId:
+ """
+ Use this method to copy messages of any kind. Service messages and invoice messages can't
+ be copied. The method is analogous to the method :meth:`forward_message`, but the copied
+ message doesn't have a link to the original message.
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
+ of the target channel (in the format ``@channelusername``).
+ from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the
+ original message was sent (or channel username in the format ``@channelusername``).
+ message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id.
+ caption (:obj:`str`, optional): New caption for media, 0-1024 characters after
+ entities parsing. If not specified, the original caption is kept.
+ parse_mode (:obj:`str`, optional): Mode for parsing entities in the new caption. See
+ the constants in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (:class:`telegram.utils.types.SLT[MessageEntity]`): List of special
+ entities that appear in the new caption, which can be specified instead of
+ parse_mode
+ disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
+ receive a notification with no sound.
+ protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
+ forwarding and saving.
+
+ .. versionadded:: 13.10
+
+ reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
+ original message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
+ reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options.
+ A JSON-serialized object for an inline keyboard, custom reply keyboard,
+ instructions to remove reply keyboard or to force a reply from the user.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
+ Telegram API.
+
+ Returns:
+ :class:`telegram.MessageId`: On success
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {
+ 'chat_id': chat_id,
+ 'from_chat_id': from_chat_id,
+ 'message_id': message_id,
+ 'parse_mode': parse_mode,
+ 'disable_notification': disable_notification,
+ 'allow_sending_without_reply': allow_sending_without_reply,
+ }
+ if caption is not None:
+ data['caption'] = caption
+ if caption_entities:
+ data['caption_entities'] = caption_entities
+ if reply_to_message_id:
+ data['reply_to_message_id'] = reply_to_message_id
+ if protect_content:
+ data['protect_content'] = protect_content
+ if reply_markup:
+ if isinstance(reply_markup, ReplyMarkup):
+ # We need to_json() instead of to_dict() here, because reply_markups may be
+ # attached to media messages, which aren't json dumped by utils.request
+ data['reply_markup'] = reply_markup.to_json()
+ else:
+ data['reply_markup'] = reply_markup
+
+ result = self._post('copyMessage', data, timeout=timeout, api_kwargs=api_kwargs)
+ return MessageId.de_json(result, self) # type: ignore[return-value, arg-type]
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data: JSONDict = {'id': self.id, 'username': self.username, 'first_name': self.first_name}
if self.last_name:
data['last_name'] = self.last_name
return data
- def __reduce__(self):
- return (self.__class__, (self.token, self.base_url.replace(self.token, ''),
- self.base_file_url.replace(self.token, '')))
+ def __eq__(self, other: object) -> bool:
+ return self.bot == other
+
+ def __hash__(self) -> int:
+ return hash(self.bot)
# camelCase aliases
getMe = get_me
- """Alias for :attr:`get_me`"""
+ """Alias for :meth:`get_me`"""
sendMessage = send_message
- """Alias for :attr:`send_message`"""
+ """Alias for :meth:`send_message`"""
deleteMessage = delete_message
- """Alias for :attr:`delete_message`"""
+ """Alias for :meth:`delete_message`"""
forwardMessage = forward_message
- """Alias for :attr:`forward_message`"""
+ """Alias for :meth:`forward_message`"""
sendPhoto = send_photo
- """Alias for :attr:`send_photo`"""
+ """Alias for :meth:`send_photo`"""
sendAudio = send_audio
- """Alias for :attr:`send_audio`"""
+ """Alias for :meth:`send_audio`"""
sendDocument = send_document
- """Alias for :attr:`send_document`"""
+ """Alias for :meth:`send_document`"""
sendSticker = send_sticker
- """Alias for :attr:`send_sticker`"""
+ """Alias for :meth:`send_sticker`"""
sendVideo = send_video
- """Alias for :attr:`send_video`"""
+ """Alias for :meth:`send_video`"""
sendAnimation = send_animation
- """Alias for :attr:`send_animation`"""
+ """Alias for :meth:`send_animation`"""
sendVoice = send_voice
- """Alias for :attr:`send_voice`"""
+ """Alias for :meth:`send_voice`"""
sendVideoNote = send_video_note
- """Alias for :attr:`send_video_note`"""
+ """Alias for :meth:`send_video_note`"""
sendMediaGroup = send_media_group
- """Alias for :attr:`send_media_group`"""
+ """Alias for :meth:`send_media_group`"""
sendLocation = send_location
- """Alias for :attr:`send_location`"""
+ """Alias for :meth:`send_location`"""
editMessageLiveLocation = edit_message_live_location
- """Alias for :attr:`edit_message_live_location`"""
+ """Alias for :meth:`edit_message_live_location`"""
stopMessageLiveLocation = stop_message_live_location
- """Alias for :attr:`stop_message_live_location`"""
+ """Alias for :meth:`stop_message_live_location`"""
sendVenue = send_venue
- """Alias for :attr:`send_venue`"""
+ """Alias for :meth:`send_venue`"""
sendContact = send_contact
- """Alias for :attr:`send_contact`"""
+ """Alias for :meth:`send_contact`"""
sendGame = send_game
- """Alias for :attr:`send_game`"""
+ """Alias for :meth:`send_game`"""
sendChatAction = send_chat_action
- """Alias for :attr:`send_chat_action`"""
+ """Alias for :meth:`send_chat_action`"""
answerInlineQuery = answer_inline_query
- """Alias for :attr:`answer_inline_query`"""
+ """Alias for :meth:`answer_inline_query`"""
getUserProfilePhotos = get_user_profile_photos
- """Alias for :attr:`get_user_profile_photos`"""
+ """Alias for :meth:`get_user_profile_photos`"""
getFile = get_file
- """Alias for :attr:`get_file`"""
+ """Alias for :meth:`get_file`"""
+ banChatMember = ban_chat_member
+ """Alias for :meth:`ban_chat_member`"""
+ banChatSenderChat = ban_chat_sender_chat
+ """Alias for :meth:`ban_chat_sender_chat`"""
kickChatMember = kick_chat_member
- """Alias for :attr:`kick_chat_member`"""
+ """Alias for :meth:`kick_chat_member`"""
unbanChatMember = unban_chat_member
- """Alias for :attr:`unban_chat_member`"""
+ """Alias for :meth:`unban_chat_member`"""
+ unbanChatSenderChat = unban_chat_sender_chat
+ """Alias for :meth:`unban_chat_sender_chat`"""
answerCallbackQuery = answer_callback_query
- """Alias for :attr:`answer_callback_query`"""
+ """Alias for :meth:`answer_callback_query`"""
editMessageText = edit_message_text
- """Alias for :attr:`edit_message_text`"""
+ """Alias for :meth:`edit_message_text`"""
editMessageCaption = edit_message_caption
- """Alias for :attr:`edit_message_caption`"""
+ """Alias for :meth:`edit_message_caption`"""
editMessageMedia = edit_message_media
- """Alias for :attr:`edit_message_media`"""
+ """Alias for :meth:`edit_message_media`"""
editMessageReplyMarkup = edit_message_reply_markup
- """Alias for :attr:`edit_message_reply_markup`"""
+ """Alias for :meth:`edit_message_reply_markup`"""
getUpdates = get_updates
- """Alias for :attr:`get_updates`"""
+ """Alias for :meth:`get_updates`"""
setWebhook = set_webhook
- """Alias for :attr:`set_webhook`"""
+ """Alias for :meth:`set_webhook`"""
deleteWebhook = delete_webhook
- """Alias for :attr:`delete_webhook`"""
+ """Alias for :meth:`delete_webhook`"""
leaveChat = leave_chat
- """Alias for :attr:`leave_chat`"""
+ """Alias for :meth:`leave_chat`"""
getChat = get_chat
- """Alias for :attr:`get_chat`"""
+ """Alias for :meth:`get_chat`"""
getChatAdministrators = get_chat_administrators
- """Alias for :attr:`get_chat_administrators`"""
+ """Alias for :meth:`get_chat_administrators`"""
getChatMember = get_chat_member
- """Alias for :attr:`get_chat_member`"""
+ """Alias for :meth:`get_chat_member`"""
setChatStickerSet = set_chat_sticker_set
- """Alias for :attr:`set_chat_sticker_set`"""
+ """Alias for :meth:`set_chat_sticker_set`"""
deleteChatStickerSet = delete_chat_sticker_set
- """Alias for :attr:`delete_chat_sticker_set`"""
+ """Alias for :meth:`delete_chat_sticker_set`"""
+ getChatMemberCount = get_chat_member_count
+ """Alias for :meth:`get_chat_member_count`"""
getChatMembersCount = get_chat_members_count
- """Alias for :attr:`get_chat_members_count`"""
+ """Alias for :meth:`get_chat_members_count`"""
getWebhookInfo = get_webhook_info
- """Alias for :attr:`get_webhook_info`"""
+ """Alias for :meth:`get_webhook_info`"""
setGameScore = set_game_score
- """Alias for :attr:`set_game_score`"""
+ """Alias for :meth:`set_game_score`"""
getGameHighScores = get_game_high_scores
- """Alias for :attr:`get_game_high_scores`"""
+ """Alias for :meth:`get_game_high_scores`"""
sendInvoice = send_invoice
- """Alias for :attr:`send_invoice`"""
+ """Alias for :meth:`send_invoice`"""
answerShippingQuery = answer_shipping_query
- """Alias for :attr:`answer_shipping_query`"""
+ """Alias for :meth:`answer_shipping_query`"""
answerPreCheckoutQuery = answer_pre_checkout_query
- """Alias for :attr:`answer_pre_checkout_query`"""
+ """Alias for :meth:`answer_pre_checkout_query`"""
restrictChatMember = restrict_chat_member
- """Alias for :attr:`restrict_chat_member`"""
+ """Alias for :meth:`restrict_chat_member`"""
promoteChatMember = promote_chat_member
- """Alias for :attr:`promote_chat_member`"""
+ """Alias for :meth:`promote_chat_member`"""
+ setChatPermissions = set_chat_permissions
+ """Alias for :meth:`set_chat_permissions`"""
+ setChatAdministratorCustomTitle = set_chat_administrator_custom_title
+ """Alias for :meth:`set_chat_administrator_custom_title`"""
exportChatInviteLink = export_chat_invite_link
- """Alias for :attr:`export_chat_invite_link`"""
+ """Alias for :meth:`export_chat_invite_link`"""
+ createChatInviteLink = create_chat_invite_link
+ """Alias for :meth:`create_chat_invite_link`"""
+ editChatInviteLink = edit_chat_invite_link
+ """Alias for :meth:`edit_chat_invite_link`"""
+ revokeChatInviteLink = revoke_chat_invite_link
+ """Alias for :meth:`revoke_chat_invite_link`"""
+ approveChatJoinRequest = approve_chat_join_request
+ """Alias for :meth:`approve_chat_join_request`"""
+ declineChatJoinRequest = decline_chat_join_request
+ """Alias for :meth:`decline_chat_join_request`"""
setChatPhoto = set_chat_photo
- """Alias for :attr:`set_chat_photo`"""
+ """Alias for :meth:`set_chat_photo`"""
deleteChatPhoto = delete_chat_photo
- """Alias for :attr:`delete_chat_photo`"""
+ """Alias for :meth:`delete_chat_photo`"""
setChatTitle = set_chat_title
- """Alias for :attr:`set_chat_title`"""
+ """Alias for :meth:`set_chat_title`"""
setChatDescription = set_chat_description
- """Alias for :attr:`set_chat_description`"""
+ """Alias for :meth:`set_chat_description`"""
pinChatMessage = pin_chat_message
- """Alias for :attr:`pin_chat_message`"""
+ """Alias for :meth:`pin_chat_message`"""
unpinChatMessage = unpin_chat_message
- """Alias for :attr:`unpin_chat_message`"""
+ """Alias for :meth:`unpin_chat_message`"""
+ unpinAllChatMessages = unpin_all_chat_messages
+ """Alias for :meth:`unpin_all_chat_messages`"""
getStickerSet = get_sticker_set
- """Alias for :attr:`get_sticker_set`"""
+ """Alias for :meth:`get_sticker_set`"""
uploadStickerFile = upload_sticker_file
- """Alias for :attr:`upload_sticker_file`"""
+ """Alias for :meth:`upload_sticker_file`"""
createNewStickerSet = create_new_sticker_set
- """Alias for :attr:`create_new_sticker_set`"""
+ """Alias for :meth:`create_new_sticker_set`"""
addStickerToSet = add_sticker_to_set
- """Alias for :attr:`add_sticker_to_set`"""
+ """Alias for :meth:`add_sticker_to_set`"""
setStickerPositionInSet = set_sticker_position_in_set
- """Alias for :attr:`set_sticker_position_in_set`"""
+ """Alias for :meth:`set_sticker_position_in_set`"""
deleteStickerFromSet = delete_sticker_from_set
- """Alias for :attr:`delete_sticker_from_set`"""
+ """Alias for :meth:`delete_sticker_from_set`"""
+ setStickerSetThumb = set_sticker_set_thumb
+ """Alias for :meth:`set_sticker_set_thumb`"""
setPassportDataErrors = set_passport_data_errors
- """Alias for :attr:`set_passport_data_errors`"""
+ """Alias for :meth:`set_passport_data_errors`"""
+ sendPoll = send_poll
+ """Alias for :meth:`send_poll`"""
+ stopPoll = stop_poll
+ """Alias for :meth:`stop_poll`"""
+ sendDice = send_dice
+ """Alias for :meth:`send_dice`"""
+ getMyCommands = get_my_commands
+ """Alias for :meth:`get_my_commands`"""
+ setMyCommands = set_my_commands
+ """Alias for :meth:`set_my_commands`"""
+ deleteMyCommands = delete_my_commands
+ """Alias for :meth:`delete_my_commands`"""
+ logOut = log_out
+ """Alias for :meth:`log_out`"""
+ copyMessage = copy_message
+ """Alias for :meth:`copy_message`"""
diff --git a/telegramer/include/telegram/botcommand.py b/telegramer/include/telegram/botcommand.py
new file mode 100644
index 0000000..7d0f2da
--- /dev/null
+++ b/telegramer/include/telegram/botcommand.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+# pylint: disable=R0903
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram Bot Command."""
+from typing import Any
+
+from telegram import TelegramObject
+
+
+class BotCommand(TelegramObject):
+ """
+ This object represents a bot command.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`command` and :attr:`description` are equal.
+
+ Args:
+ command (:obj:`str`): Text of the command, 1-32 characters. Can contain only lowercase
+ English letters, digits and underscores.
+ description (:obj:`str`): Description of the command, 1-256 characters.
+
+ Attributes:
+ command (:obj:`str`): Text of the command.
+ description (:obj:`str`): Description of the command.
+
+ """
+
+ __slots__ = ('description', '_id_attrs', 'command')
+
+ def __init__(self, command: str, description: str, **_kwargs: Any):
+ self.command = command
+ self.description = description
+
+ self._id_attrs = (self.command, self.description)
diff --git a/telegramer/include/telegram/botcommandscope.py b/telegramer/include/telegram/botcommandscope.py
new file mode 100644
index 0000000..283f768
--- /dev/null
+++ b/telegramer/include/telegram/botcommandscope.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=W0622
+"""This module contains objects representing Telegram bot command scopes."""
+from typing import Any, Union, Optional, TYPE_CHECKING, Dict, Type
+
+from telegram import TelegramObject, constants
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+
+class BotCommandScope(TelegramObject):
+ """Base class for objects that represent the scope to which bot commands are applied.
+ Currently, the following 7 scopes are supported:
+
+ * :class:`telegram.BotCommandScopeDefault`
+ * :class:`telegram.BotCommandScopeAllPrivateChats`
+ * :class:`telegram.BotCommandScopeAllGroupChats`
+ * :class:`telegram.BotCommandScopeAllChatAdministrators`
+ * :class:`telegram.BotCommandScopeChat`
+ * :class:`telegram.BotCommandScopeChatAdministrators`
+ * :class:`telegram.BotCommandScopeChatMember`
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`type` is equal. For subclasses with additional attributes,
+ the notion of equality is overridden.
+
+ Note:
+ Please see the `official docs`_ on how Telegram determines which commands to display.
+
+ .. _`official docs`: https://core.telegram.org/bots/api#determining-list-of-commands
+
+ .. versionadded:: 13.7
+
+ Args:
+ type (:obj:`str`): Scope type.
+
+ Attributes:
+ type (:obj:`str`): Scope type.
+ """
+
+ __slots__ = ('type', '_id_attrs')
+
+ DEFAULT = constants.BOT_COMMAND_SCOPE_DEFAULT
+ """:const:`telegram.constants.BOT_COMMAND_SCOPE_DEFAULT`"""
+ ALL_PRIVATE_CHATS = constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS
+ """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS`"""
+ ALL_GROUP_CHATS = constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS
+ """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS`"""
+ ALL_CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS
+ """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS`"""
+ CHAT = constants.BOT_COMMAND_SCOPE_CHAT
+ """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT`"""
+ CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS
+ """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS`"""
+ CHAT_MEMBER = constants.BOT_COMMAND_SCOPE_CHAT_MEMBER
+ """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_MEMBER`"""
+
+ def __init__(self, type: str, **_kwargs: Any):
+ self.type = type
+ self._id_attrs = (self.type,)
+
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['BotCommandScope']:
+ """Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes
+ care of selecting the correct subclass.
+
+ Args:
+ data (Dict[:obj:`str`, ...]): The JSON data.
+ bot (:class:`telegram.Bot`): The bot associated with this object.
+
+ Returns:
+ The Telegram object.
+
+ """
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ _class_mapping: Dict[str, Type['BotCommandScope']] = {
+ cls.DEFAULT: BotCommandScopeDefault,
+ cls.ALL_PRIVATE_CHATS: BotCommandScopeAllPrivateChats,
+ cls.ALL_GROUP_CHATS: BotCommandScopeAllGroupChats,
+ cls.ALL_CHAT_ADMINISTRATORS: BotCommandScopeAllChatAdministrators,
+ cls.CHAT: BotCommandScopeChat,
+ cls.CHAT_ADMINISTRATORS: BotCommandScopeChatAdministrators,
+ cls.CHAT_MEMBER: BotCommandScopeChatMember,
+ }
+
+ if cls is BotCommandScope:
+ return _class_mapping.get(data['type'], cls)(**data, bot=bot)
+ return cls(**data)
+
+
+class BotCommandScopeDefault(BotCommandScope):
+ """Represents the default scope of bot commands. Default commands are used if no commands with
+ a `narrower scope`_ are specified for the user.
+
+ .. _`narrower scope`: https://core.telegram.org/bots/api#determining-list-of-commands
+
+ .. versionadded:: 13.7
+
+ Attributes:
+ type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.DEFAULT`.
+ """
+
+ __slots__ = ()
+
+ def __init__(self, **_kwargs: Any):
+ super().__init__(type=BotCommandScope.DEFAULT)
+
+
+class BotCommandScopeAllPrivateChats(BotCommandScope):
+ """Represents the scope of bot commands, covering all private chats.
+
+ .. versionadded:: 13.7
+
+ Attributes:
+ type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_PRIVATE_CHATS`.
+ """
+
+ __slots__ = ()
+
+ def __init__(self, **_kwargs: Any):
+ super().__init__(type=BotCommandScope.ALL_PRIVATE_CHATS)
+
+
+class BotCommandScopeAllGroupChats(BotCommandScope):
+ """Represents the scope of bot commands, covering all group and supergroup chats.
+
+ .. versionadded:: 13.7
+
+ Attributes:
+ type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_GROUP_CHATS`.
+ """
+
+ __slots__ = ()
+
+ def __init__(self, **_kwargs: Any):
+ super().__init__(type=BotCommandScope.ALL_GROUP_CHATS)
+
+
+class BotCommandScopeAllChatAdministrators(BotCommandScope):
+ """Represents the scope of bot commands, covering all group and supergroup chat administrators.
+
+ .. versionadded:: 13.7
+
+ Attributes:
+ type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_CHAT_ADMINISTRATORS`.
+ """
+
+ __slots__ = ()
+
+ def __init__(self, **_kwargs: Any):
+ super().__init__(type=BotCommandScope.ALL_CHAT_ADMINISTRATORS)
+
+
+class BotCommandScopeChat(BotCommandScope):
+ """Represents the scope of bot commands, covering a specific chat.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`type` and :attr:`chat_id` are equal.
+
+ .. versionadded:: 13.7
+
+ Args:
+ chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
+ target supergroup (in the format ``@supergroupusername``)
+
+ Attributes:
+ type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT`.
+ chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
+ target supergroup (in the format ``@supergroupusername``)
+ """
+
+ __slots__ = ('chat_id',)
+
+ def __init__(self, chat_id: Union[str, int], **_kwargs: Any):
+ super().__init__(type=BotCommandScope.CHAT)
+ self.chat_id = (
+ chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id)
+ )
+ self._id_attrs = (self.type, self.chat_id)
+
+
+class BotCommandScopeChatAdministrators(BotCommandScope):
+ """Represents the scope of bot commands, covering all administrators of a specific group or
+ supergroup chat.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`type` and :attr:`chat_id` are equal.
+
+ .. versionadded:: 13.7
+
+ Args:
+ chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
+ target supergroup (in the format ``@supergroupusername``)
+
+ Attributes:
+ type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_ADMINISTRATORS`.
+ chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
+ target supergroup (in the format ``@supergroupusername``)
+ """
+
+ __slots__ = ('chat_id',)
+
+ def __init__(self, chat_id: Union[str, int], **_kwargs: Any):
+ super().__init__(type=BotCommandScope.CHAT_ADMINISTRATORS)
+ self.chat_id = (
+ chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id)
+ )
+ self._id_attrs = (self.type, self.chat_id)
+
+
+class BotCommandScopeChatMember(BotCommandScope):
+ """Represents the scope of bot commands, covering a specific member of a group or supergroup
+ chat.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`type`, :attr:`chat_id` and :attr:`user_id` are equal.
+
+ .. versionadded:: 13.7
+
+ Args:
+ chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
+ target supergroup (in the format ``@supergroupusername``)
+ user_id (:obj:`int`): Unique identifier of the target user.
+
+ Attributes:
+ type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_MEMBER`.
+ chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
+ target supergroup (in the format ``@supergroupusername``)
+ user_id (:obj:`int`): Unique identifier of the target user.
+ """
+
+ __slots__ = ('chat_id', 'user_id')
+
+ def __init__(self, chat_id: Union[str, int], user_id: int, **_kwargs: Any):
+ super().__init__(type=BotCommandScope.CHAT_MEMBER)
+ self.chat_id = (
+ chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id)
+ )
+ self.user_id = int(user_id)
+ self._id_attrs = (self.type, self.chat_id, self.user_id)
diff --git a/telegramer/include/telegram/callbackquery.py b/telegramer/include/telegram/callbackquery.py
index 54e5b75..b783636 100644
--- a/telegramer/include/telegram/callbackquery.py
+++ b/telegramer/include/telegram/callbackquery.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,9 +16,23 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=W0622
"""This module contains an object that represents a Telegram CallbackQuery"""
+from typing import TYPE_CHECKING, Any, List, Optional, Union, Tuple, ClassVar
-from telegram import TelegramObject, Message, User
+from telegram import Message, TelegramObject, User, Location, ReplyMarkup, constants
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput, DVInput
+
+if TYPE_CHECKING:
+ from telegram import (
+ Bot,
+ GameHighScore,
+ InlineKeyboardMarkup,
+ MessageId,
+ InputMedia,
+ MessageEntity,
+ )
class CallbackQuery(TelegramObject):
@@ -29,58 +43,81 @@ class CallbackQuery(TelegramObject):
:attr:`message` will be present. If the button was attached to a message sent via the bot (in
inline mode), the field :attr:`inline_message_id` will be present.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
+
Note:
- * In Python `from` is a reserved word, use `from_user` instead.
+ * In Python ``from`` is a reserved word, use ``from_user`` instead.
* Exactly one of the fields :attr:`data` or :attr:`game_short_name` will be present.
+ * After the user presses an inline button, Telegram clients will display a progress bar
+ until you call :attr:`answer`. It is, therefore, necessary to react
+ by calling :attr:`telegram.Bot.answer_callback_query` even if no notification to the user
+ is needed (e.g., without specifying any of the optional parameters).
+ * If you're using :attr:`Bot.arbitrary_callback_data`, :attr:`data` may be an instance
+ of :class:`telegram.ext.InvalidCallbackData`. This will be the case, if the data
+ associated with the button triggering the :class:`telegram.CallbackQuery` was already
+ deleted or if :attr:`data` was manipulated by a malicious client.
+
+ .. versionadded:: 13.6
- Attributes:
- id (:obj:`str`): Unique identifier for this query.
- from_user (:class:`telegram.User`): Sender.
- message (:class:`telegram.Message`): Optional. Message with the callback button that
- originated the query.
- inline_message_id (:obj:`str`): Optional. Identifier of the message sent via the bot in
- inline mode, that originated the query.
- chat_instance (:obj:`str`): Optional. Global identifier, uniquely corresponding to the chat
- to which the message with the callback button was sent.
- data (:obj:`str`): Optional. Data associated with the callback button.
- game_short_name (:obj:`str`): Optional. Short name of a Game to be returned.
Args:
id (:obj:`str`): Unique identifier for this query.
from_user (:class:`telegram.User`): Sender.
+ chat_instance (:obj:`str`): Global identifier, uniquely corresponding to the chat to which
+ the message with the callback button was sent. Useful for high scores in games.
message (:class:`telegram.Message`, optional): Message with the callback button that
originated the query. Note that message content and message date will not be available
if the message is too old.
- inline_message_id (:obj:`str`, optional): Identifier of the message sent via the bot in
- inline mode, that originated the query.
- chat_instance (:obj:`str`, optional): Global identifier, uniquely corresponding to the chat
- to which the message with the callback button was sent. Useful for high scores in
- games.
data (:obj:`str`, optional): Data associated with the callback button. Be aware that a bad
client can send arbitrary data in this field.
+ inline_message_id (:obj:`str`, optional): Identifier of the message sent via the bot in
+ inline mode, that originated the query.
game_short_name (:obj:`str`, optional): Short name of a Game to be returned, serves as
the unique identifier for the game
+ bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
- Note:
- After the user presses an inline button, Telegram clients will display a progress bar
- until you call :attr:`answer`. It is, therefore, necessary to react
- by calling :attr:`telegram.Bot.answer_callback_query` even if no notification to the user
- is needed (e.g., without specifying any of the optional parameters).
+ Attributes:
+ id (:obj:`str`): Unique identifier for this query.
+ from_user (:class:`telegram.User`): Sender.
+ chat_instance (:obj:`str`): Global identifier, uniquely corresponding to the chat to which
+ the message with the callback button was sent.
+ message (:class:`telegram.Message`): Optional. Message with the callback button that
+ originated the query.
+ data (:obj:`str` | :obj:`object`): Optional. Data associated with the callback button.
+ inline_message_id (:obj:`str`): Optional. Identifier of the message sent via the bot in
+ inline mode, that originated the query.
+ game_short_name (:obj:`str`): Optional. Short name of a Game to be returned.
+ bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
"""
- def __init__(self,
- id,
- from_user,
- chat_instance,
- message=None,
- data=None,
- inline_message_id=None,
- game_short_name=None,
- bot=None,
- **kwargs):
+ __slots__ = (
+ 'bot',
+ 'game_short_name',
+ 'message',
+ 'chat_instance',
+ 'id',
+ 'from_user',
+ 'inline_message_id',
+ 'data',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ from_user: User,
+ chat_instance: str,
+ message: Message = None,
+ data: str = None,
+ inline_message_id: str = None,
+ game_short_name: str = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
- self.id = id
+ self.id = id # pylint: disable=C0103
self.from_user = from_user
self.chat_instance = chat_instance
# Optionals
@@ -94,96 +131,530 @@ def __init__(self,
self._id_attrs = (self.id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['CallbackQuery']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(CallbackQuery, cls).de_json(data, bot)
-
data['from_user'] = User.de_json(data.get('from'), bot)
data['message'] = Message.de_json(data.get('message'), bot)
return cls(bot=bot, **data)
- def answer(self, *args, **kwargs):
+ def answer(
+ self,
+ text: str = None,
+ show_alert: bool = False,
+ url: str = None,
+ cache_time: int = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Shortcut for::
bot.answer_callback_query(update.callback_query.id, *args, **kwargs)
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.answer_callback_query`.
+
Returns:
- :obj:`bool`: On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
"""
- return self.bot.answerCallbackQuery(self.id, *args, **kwargs)
-
- def edit_message_text(self, *args, **kwargs):
+ return self.bot.answer_callback_query(
+ callback_query_id=self.id,
+ text=text,
+ show_alert=show_alert,
+ url=url,
+ cache_time=cache_time,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def edit_message_text(
+ self,
+ text: str,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ ) -> Union[Message, bool]:
"""Shortcut for either::
- bot.edit_message_text(chat_id=update.callback_query.message.chat_id,
- message_id=update.callback_query.message.message_id,
- *args, **kwargs)
+ update.callback_query.message.edit_text(text, *args, **kwargs)
or::
- bot.edit_message_text(inline_message_id=update.callback_query.inline_message_id,
+ bot.edit_message_text(text, inline_message_id=update.callback_query.inline_message_id,
*args, **kwargs)
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_message_text` and :meth:`telegram.Message.edit_text`.
+
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
- edited Message is returned, otherwise ``True`` is returned.
+ edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
return self.bot.edit_message_text(
- inline_message_id=self.inline_message_id, *args, **kwargs)
- else:
- return self.bot.edit_message_text(
- chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs)
-
- def edit_message_caption(self, *args, **kwargs):
+ inline_message_id=self.inline_message_id,
+ text=text,
+ parse_mode=parse_mode,
+ disable_web_page_preview=disable_web_page_preview,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ entities=entities,
+ chat_id=None,
+ message_id=None,
+ )
+ return self.message.edit_text(
+ text=text,
+ parse_mode=parse_mode,
+ disable_web_page_preview=disable_web_page_preview,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ entities=entities,
+ )
+
+ def edit_message_caption(
+ self,
+ caption: str = None,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ ) -> Union[Message, bool]:
"""Shortcut for either::
- bot.edit_message_caption(chat_id=update.callback_query.message.chat_id,
- message_id=update.callback_query.message.message_id,
- *args, **kwargs)
+ update.callback_query.message.edit_caption(caption, *args, **kwargs)
or::
- bot.edit_message_caption(inline_message_id=update.callback_query.inline_message_id,
+ bot.edit_message_caption(caption=caption
+ inline_message_id=update.callback_query.inline_message_id,
*args, **kwargs)
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_message_caption` and :meth:`telegram.Message.edit_caption`.
+
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
- edited Message is returned, otherwise ``True`` is returned.
+ edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
return self.bot.edit_message_caption(
- inline_message_id=self.inline_message_id, *args, **kwargs)
- else:
- return self.bot.edit_message_caption(
- chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs)
-
- def edit_message_reply_markup(self, *args, **kwargs):
+ caption=caption,
+ inline_message_id=self.inline_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ api_kwargs=api_kwargs,
+ caption_entities=caption_entities,
+ chat_id=None,
+ message_id=None,
+ )
+ return self.message.edit_caption(
+ caption=caption,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ api_kwargs=api_kwargs,
+ caption_entities=caption_entities,
+ )
+
+ def edit_message_reply_markup(
+ self,
+ reply_markup: Optional['InlineKeyboardMarkup'] = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union[Message, bool]:
"""Shortcut for either::
- bot.edit_message_replyMarkup(chat_id=update.callback_query.message.chat_id,
- message_id=update.callback_query.message.message_id,
- *args, **kwargs)
+ update.callback_query.message.edit_reply_markup(
+ reply_markup=reply_markup,
+ *args,
+ **kwargs
+ )
or::
- bot.edit_message_reply_markup(inline_message_id=update.callback_query.inline_message_id,
- *args, **kwargs)
+ bot.edit_message_reply_markup
+ inline_message_id=update.callback_query.inline_message_id,
+ reply_markup=reply_markup,
+ *args,
+ **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_message_reply_markup` and
+ :meth:`telegram.Message.edit_reply_markup`.
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
- edited Message is returned, otherwise ``True`` is returned.
+ edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
return self.bot.edit_message_reply_markup(
- inline_message_id=self.inline_message_id, *args, **kwargs)
- else:
- return self.bot.edit_message_reply_markup(
- chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs)
+ reply_markup=reply_markup,
+ inline_message_id=self.inline_message_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ chat_id=None,
+ message_id=None,
+ )
+ return self.message.edit_reply_markup(
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def edit_message_media(
+ self,
+ media: 'InputMedia' = None,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union[Message, bool]:
+ """Shortcut for either::
+
+ update.callback_query.message.edit_media(*args, **kwargs)
+
+ or::
+
+ bot.edit_message_media(inline_message_id=update.callback_query.inline_message_id,
+ *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_message_media` and :meth:`telegram.Message.edit_media`.
+
+ Returns:
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise :obj:`True` is returned.
+
+ """
+ if self.inline_message_id:
+ return self.bot.edit_message_media(
+ inline_message_id=self.inline_message_id,
+ media=media,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ chat_id=None,
+ message_id=None,
+ )
+ return self.message.edit_media(
+ media=media,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def edit_message_live_location(
+ self,
+ latitude: float = None,
+ longitude: float = None,
+ location: Location = None,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ horizontal_accuracy: float = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ ) -> Union[Message, bool]:
+ """Shortcut for either::
+
+ update.callback_query.message.edit_live_location(*args, **kwargs)
+
+ or::
+
+ bot.edit_message_live_location(
+ inline_message_id=update.callback_query.inline_message_id,
+ *args, **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_message_live_location` and
+ :meth:`telegram.Message.edit_live_location`.
+
+ Returns:
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise :obj:`True` is returned.
+
+ """
+ if self.inline_message_id:
+ return self.bot.edit_message_live_location(
+ inline_message_id=self.inline_message_id,
+ latitude=latitude,
+ longitude=longitude,
+ location=location,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ horizontal_accuracy=horizontal_accuracy,
+ heading=heading,
+ proximity_alert_radius=proximity_alert_radius,
+ chat_id=None,
+ message_id=None,
+ )
+ return self.message.edit_live_location(
+ latitude=latitude,
+ longitude=longitude,
+ location=location,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ horizontal_accuracy=horizontal_accuracy,
+ heading=heading,
+ proximity_alert_radius=proximity_alert_radius,
+ )
+
+ def stop_message_live_location(
+ self,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union[Message, bool]:
+ """Shortcut for either::
+
+ update.callback_query.message.stop_live_location(*args, **kwargs)
+
+ or::
+
+ bot.stop_message_live_location(
+ inline_message_id=update.callback_query.inline_message_id,
+ *args, **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.stop_message_live_location` and
+ :meth:`telegram.Message.stop_live_location`.
+
+ Returns:
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise :obj:`True` is returned.
+
+ """
+ if self.inline_message_id:
+ return self.bot.stop_message_live_location(
+ inline_message_id=self.inline_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ chat_id=None,
+ message_id=None,
+ )
+ return self.message.stop_live_location(
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def set_game_score(
+ self,
+ user_id: Union[int, str],
+ score: int,
+ force: bool = None,
+ disable_edit_message: bool = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union[Message, bool]:
+ """Shortcut for either::
+
+ update.callback_query.message.set_game_score(*args, **kwargs)
+
+ or::
+
+ bot.set_game_score(inline_message_id=update.callback_query.inline_message_id,
+ *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.set_game_score` and :meth:`telegram.Message.set_game_score`.
+
+ Returns:
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise :obj:`True` is returned.
+
+ """
+ if self.inline_message_id:
+ return self.bot.set_game_score(
+ inline_message_id=self.inline_message_id,
+ user_id=user_id,
+ score=score,
+ force=force,
+ disable_edit_message=disable_edit_message,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ chat_id=None,
+ message_id=None,
+ )
+ return self.message.set_game_score(
+ user_id=user_id,
+ score=score,
+ force=force,
+ disable_edit_message=disable_edit_message,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def get_game_high_scores(
+ self,
+ user_id: Union[int, str],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> List['GameHighScore']:
+ """Shortcut for either::
+
+ update.callback_query.message.get_game_high_score(*args, **kwargs)
+
+ or::
+
+ bot.get_game_high_scores(inline_message_id=update.callback_query.inline_message_id,
+ *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.get_game_high_scores` and :meth:`telegram.Message.get_game_high_score`.
+
+ Returns:
+ List[:class:`telegram.GameHighScore`]
+
+ """
+ if self.inline_message_id:
+ return self.bot.get_game_high_scores(
+ inline_message_id=self.inline_message_id,
+ user_id=user_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ chat_id=None,
+ message_id=None,
+ )
+ return self.message.get_game_high_scores(
+ user_id=user_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def delete_message(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ update.callback_query.message.delete(*args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Message.delete`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.message.delete(
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def pin_message(
+ self,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ update.callback_query.message.pin(*args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Message.pin`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.message.pin(
+ disable_notification=disable_notification,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def unpin_message(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ update.callback_query.message.unpin(*args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Message.unpin`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.message.unpin(
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def copy_message(
+ self,
+ chat_id: Union[int, str],
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> 'MessageId':
+ """Shortcut for::
+
+ update.callback_query.message.copy(
+ chat_id,
+ from_chat_id=update.message.chat_id,
+ message_id=update.message.message_id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Message.copy`.
+
+ Returns:
+ :class:`telegram.MessageId`: On success, returns the MessageId of the sent message.
+
+ """
+ return self.message.copy(
+ chat_id=chat_id,
+ caption=caption,
+ parse_mode=parse_mode,
+ caption_entities=caption_entities,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ allow_sending_without_reply=allow_sending_without_reply,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ MAX_ANSWER_TEXT_LENGTH: ClassVar[int] = constants.MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH
+ """
+ :const:`telegram.constants.MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH`
+
+ .. versionadded:: 13.2
+ """
diff --git a/telegramer/include/telegram/chat.py b/telegramer/include/telegram/chat.py
index 5938c7c..b5dbfe4 100644
--- a/telegramer/include/telegram/chat.py
+++ b/telegramer/include/telegram/chat.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python
-# pylint: disable=C0103,W0622
+# pylint: disable=W0622
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,29 +18,52 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Chat."""
-
-from telegram import TelegramObject, ChatPhoto
+import warnings
+from datetime import datetime
+from typing import TYPE_CHECKING, List, Optional, ClassVar, Union, Tuple, Any
+
+from telegram import ChatPhoto, TelegramObject, constants
+from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput
+from telegram.utils.deprecate import TelegramDeprecationWarning
+
+from .chatpermissions import ChatPermissions
+from .chatlocation import ChatLocation
+from .utils.helpers import DEFAULT_NONE, DEFAULT_20
+
+if TYPE_CHECKING:
+ from telegram import (
+ Bot,
+ ChatMember,
+ ChatInviteLink,
+ Message,
+ MessageId,
+ ReplyMarkup,
+ Contact,
+ InlineKeyboardMarkup,
+ Location,
+ Venue,
+ MessageEntity,
+ InputMediaAudio,
+ InputMediaDocument,
+ InputMediaPhoto,
+ InputMediaVideo,
+ PhotoSize,
+ Audio,
+ Document,
+ Animation,
+ LabeledPrice,
+ Sticker,
+ Video,
+ VideoNote,
+ Voice,
+ )
class Chat(TelegramObject):
"""This object represents a chat.
- Attributes:
- id (:obj:`int`): Unique identifier for this chat.
- type (:obj:`str`): Type of chat.
- title (:obj:`str`): Optional. Title, for supergroups, channels and group chats.
- username (:obj:`str`): Optional. Username.
- first_name (:obj:`str`): Optional. First name of the other party in a private chat.
- last_name (:obj:`str`): Optional. Last name of the other party in a private chat.
- all_members_are_administrators (:obj:`bool`): Optional.
- photo (:class:`telegram.ChatPhoto`): Optional. Chat photo.
- description (:obj:`str`): Optional. Description, for supergroups and channel chats.
- invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats.
- pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups.
- Returned only in get_chat.
- sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set.
- can_set_sticker_set (:obj:`bool`): Optional. ``True``, if the bot can change group the
- sticker set.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
Args:
id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits
@@ -54,286 +77,1739 @@ class Chat(TelegramObject):
available.
first_name(:obj:`str`, optional): First name of the other party in a private chat.
last_name(:obj:`str`, optional): Last name of the other party in a private chat.
- all_members_are_administrators (:obj:`bool`, optional): True if a group has `All Members
- Are Admins` enabled.
- photo (:class:`telegram.ChatPhoto`, optional): Chat photo. Returned only in getChat.
- description (:obj:`str`, optional): Description, for supergroups and channel chats.
- Returned only in get_chat.
- invite_link (:obj:`str`, optional): Chat invite link, for supergroups and channel chats.
- Returned only in get_chat.
- pinned_message (:class:`telegram.Message`, optional): Pinned message, for supergroups.
- Returned only in get_chat.
+ photo (:class:`telegram.ChatPhoto`, optional): Chat photo.
+ Returned only in :meth:`telegram.Bot.get_chat`.
+ bio (:obj:`str`, optional): Bio of the other party in a private chat. Returned only in
+ :meth:`telegram.Bot.get_chat`.
+ has_private_forwards (:obj:`bool`, optional): :obj:`True`, if privacy settings of the other
+ party in the private chat allows to use ``tg://user?id=`` links only in chats
+ with the user. Returned only in :meth:`telegram.Bot.get_chat`.
+
+ .. versionadded:: 13.9
+ description (:obj:`str`, optional): Description, for groups, supergroups and channel chats.
+ Returned only in :meth:`telegram.Bot.get_chat`.
+ invite_link (:obj:`str`, optional): Primary invite link, for groups, supergroups and
+ channel. Returned only in :meth:`telegram.Bot.get_chat`.
+ pinned_message (:class:`telegram.Message`, optional): The most recent pinned message
+ (by sending date). Returned only in :meth:`telegram.Bot.get_chat`.
+ permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
+ for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`.
+ slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between
+ consecutive messages sent by each unprivileged user.
+ Returned only in :meth:`telegram.Bot.get_chat`.
+ message_auto_delete_time (:obj:`int`, optional): The time after which all messages sent to
+ the chat will be automatically deleted; in seconds. Returned only in
+ :meth:`telegram.Bot.get_chat`.
+
+ .. versionadded:: 13.4
+ has_protected_content (:obj:`bool`, optional): :obj:`True`, if messages from the chat can't
+ be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`.
+
+ .. versionadded:: 13.9
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
- sticker_set_name (:obj:`str`, optional): For supergroups, name of Group sticker set.
- Returned only in get_chat.
- can_set_sticker_set (:obj:`bool`, optional): ``True``, if the bot can change group the
- sticker set. Returned only in get_chat.
+ sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set.
+ Returned only in :meth:`telegram.Bot.get_chat`.
+ can_set_sticker_set (:obj:`bool`, optional): :obj:`True`, if the bot can change group the
+ sticker set. Returned only in :meth:`telegram.Bot.get_chat`.
+ linked_chat_id (:obj:`int`, optional): Unique identifier for the linked chat, i.e. the
+ discussion group identifier for a channel and vice versa; for supergroups and channel
+ chats. Returned only in :meth:`telegram.Bot.get_chat`.
+ location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which
+ the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ id (:obj:`int`): Unique identifier for this chat.
+ type (:obj:`str`): Type of chat.
+ title (:obj:`str`): Optional. Title, for supergroups, channels and group chats.
+ username (:obj:`str`): Optional. Username.
+ first_name (:obj:`str`): Optional. First name of the other party in a private chat.
+ last_name (:obj:`str`): Optional. Last name of the other party in a private chat.
+ photo (:class:`telegram.ChatPhoto`): Optional. Chat photo.
+ bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in
+ :meth:`telegram.Bot.get_chat`.
+ has_private_forwards (:obj:`bool`): Optional. :obj:`True`, if privacy settings of the other
+ party in the private chat allows to use ``tg://user?id=`` links only in chats
+ with the user.
+
+ .. versionadded:: 13.9
+ description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats.
+ invite_link (:obj:`str`): Optional. Primary invite link, for groups, supergroups and
+ channel. Returned only in :meth:`telegram.Bot.get_chat`.
+ pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message
+ (by sending date). Returned only in :meth:`telegram.Bot.get_chat`.
+ permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
+ for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`.
+ slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between
+ consecutive messages sent by each unprivileged user. Returned only in
+ :meth:`telegram.Bot.get_chat`.
+ message_auto_delete_time (:obj:`int`): Optional. The time after which all messages sent to
+ the chat will be automatically deleted; in seconds. Returned only in
+ :meth:`telegram.Bot.get_chat`.
+
+ .. versionadded:: 13.4
+ has_protected_content (:obj:`bool`): Optional. :obj:`True`, if messages from the chat can't
+ be forwarded to other chats.
+
+ .. versionadded:: 13.9
+ sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set.
+ can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the
+ sticker set.
+ linked_chat_id (:obj:`int`): Optional. Unique identifier for the linked chat, i.e. the
+ discussion group identifier for a channel and vice versa; for supergroups and channel
+ chats. Returned only in :meth:`telegram.Bot.get_chat`.
+ location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which
+ the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
+
"""
- PRIVATE = 'private'
- """:obj:`str`: 'private'"""
- GROUP = 'group'
- """:obj:`str`: 'group'"""
- SUPERGROUP = 'supergroup'
- """:obj:`str`: 'supergroup'"""
- CHANNEL = 'channel'
- """:obj:`str`: 'channel'"""
-
- def __init__(self,
- id,
- type,
- title=None,
- username=None,
- first_name=None,
- last_name=None,
- all_members_are_administrators=None,
- bot=None,
- photo=None,
- description=None,
- invite_link=None,
- pinned_message=None,
- sticker_set_name=None,
- can_set_sticker_set=None,
- **kwargs):
+ __slots__ = (
+ 'bio',
+ 'id',
+ 'type',
+ 'last_name',
+ 'bot',
+ 'sticker_set_name',
+ 'slow_mode_delay',
+ 'location',
+ 'first_name',
+ 'permissions',
+ 'invite_link',
+ 'pinned_message',
+ 'description',
+ 'can_set_sticker_set',
+ 'username',
+ 'title',
+ 'photo',
+ 'linked_chat_id',
+ 'all_members_are_administrators',
+ 'message_auto_delete_time',
+ 'has_protected_content',
+ 'has_private_forwards',
+ '_id_attrs',
+ )
+
+ SENDER: ClassVar[str] = constants.CHAT_SENDER
+ """:const:`telegram.constants.CHAT_SENDER`
+
+ .. versionadded:: 13.5
+ """
+ PRIVATE: ClassVar[str] = constants.CHAT_PRIVATE
+ """:const:`telegram.constants.CHAT_PRIVATE`"""
+ GROUP: ClassVar[str] = constants.CHAT_GROUP
+ """:const:`telegram.constants.CHAT_GROUP`"""
+ SUPERGROUP: ClassVar[str] = constants.CHAT_SUPERGROUP
+ """:const:`telegram.constants.CHAT_SUPERGROUP`"""
+ CHANNEL: ClassVar[str] = constants.CHAT_CHANNEL
+ """:const:`telegram.constants.CHAT_CHANNEL`"""
+
+ def __init__(
+ self,
+ id: int,
+ type: str,
+ title: str = None,
+ username: str = None,
+ first_name: str = None,
+ last_name: str = None,
+ bot: 'Bot' = None,
+ photo: ChatPhoto = None,
+ description: str = None,
+ invite_link: str = None,
+ pinned_message: 'Message' = None,
+ permissions: ChatPermissions = None,
+ sticker_set_name: str = None,
+ can_set_sticker_set: bool = None,
+ slow_mode_delay: int = None,
+ bio: str = None,
+ linked_chat_id: int = None,
+ location: ChatLocation = None,
+ message_auto_delete_time: int = None,
+ has_private_forwards: bool = None,
+ has_protected_content: bool = None,
+ **_kwargs: Any,
+ ):
# Required
- self.id = int(id)
+ self.id = int(id) # pylint: disable=C0103
self.type = type
# Optionals
self.title = title
self.username = username
self.first_name = first_name
self.last_name = last_name
- self.all_members_are_administrators = all_members_are_administrators
+ # TODO: Remove (also from tests), when Telegram drops this completely
+ self.all_members_are_administrators = _kwargs.get('all_members_are_administrators')
self.photo = photo
+ self.bio = bio
+ self.has_private_forwards = has_private_forwards
self.description = description
self.invite_link = invite_link
self.pinned_message = pinned_message
+ self.permissions = permissions
+ self.slow_mode_delay = slow_mode_delay
+ self.message_auto_delete_time = (
+ int(message_auto_delete_time) if message_auto_delete_time is not None else None
+ )
+ self.has_protected_content = has_protected_content
self.sticker_set_name = sticker_set_name
self.can_set_sticker_set = can_set_sticker_set
+ self.linked_chat_id = linked_chat_id
+ self.location = location
self.bot = bot
self._id_attrs = (self.id,)
@property
- def link(self):
+ def full_name(self) -> Optional[str]:
+ """
+ :obj:`str`: Convenience property. If :attr:`first_name` is not :obj:`None` gives,
+ :attr:`first_name` followed by (if available) :attr:`last_name`.
+
+ Note:
+ :attr:`full_name` will always be :obj:`None`, if the chat is a (super)group or
+ channel.
+
+ .. versionadded:: 13.2
+ """
+ if not self.first_name:
+ return None
+ if self.last_name:
+ return f'{self.first_name} {self.last_name}'
+ return self.first_name
+
+ @property
+ def link(self) -> Optional[str]:
""":obj:`str`: Convenience property. If the chat has a :attr:`username`, returns a t.me
- link of the chat."""
+ link of the chat.
+ """
if self.username:
- return "https://t.me/{}".format(self.username)
+ return f"https://t.me/{self.username}"
return None
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Chat']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
data['photo'] = ChatPhoto.de_json(data.get('photo'), bot)
- from telegram import Message
+ from telegram import Message # pylint: disable=C0415
+
data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot)
+ data['permissions'] = ChatPermissions.de_json(data.get('permissions'), bot)
+ data['location'] = ChatLocation.de_json(data.get('location'), bot)
return cls(bot=bot, **data)
- def send_action(self, *args, **kwargs):
+ def leave(self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None) -> bool:
+ """Shortcut for::
+
+ bot.leave_chat(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.leave_chat`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.leave_chat(
+ chat_id=self.id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def get_administrators(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> List['ChatMember']:
"""Shortcut for::
- bot.send_chat_action(update.message.chat.id, *args, **kwargs)
+ bot.get_chat_administrators(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.get_chat_administrators`.
Returns:
- :obj:`bool`: If the action was sent successfully.
+ List[:class:`telegram.ChatMember`]: A list of administrators in a chat. An Array of
+ :class:`telegram.ChatMember` objects that contains information about all
+ chat administrators except other bots. If the chat is a group or a supergroup
+ and no administrators were appointed, only the creator will be returned.
+
+ """
+ return self.bot.get_chat_administrators(
+ chat_id=self.id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def get_members_count(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> int:
+ """
+ Deprecated, use :func:`~telegram.Chat.get_member_count` instead.
+ .. deprecated:: 13.7
"""
+ warnings.warn(
+ '`Chat.get_members_count` is deprecated. Use `Chat.get_member_count` instead.',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+
+ return self.get_member_count(
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def get_member_count(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> int:
+ """Shortcut for::
+
+ bot.get_chat_member_count(update.effective_chat.id, *args, **kwargs)
- return self.bot.send_chat_action(self.id, *args, **kwargs)
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.get_chat_member_count`.
- def leave(self, *args, **kwargs):
+ Returns:
+ :obj:`int`
+ """
+ return self.bot.get_chat_member_count(
+ chat_id=self.id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def get_member(
+ self,
+ user_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> 'ChatMember':
"""Shortcut for::
- bot.leave_chat(update.message.chat.id, *args, **kwargs)
+ bot.get_chat_member(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_chat_member`.
Returns:
- :obj:`bool` If the action was sent successfully.
+ :class:`telegram.ChatMember`
"""
- return self.bot.leave_chat(self.id, *args, **kwargs)
+ return self.bot.get_chat_member(
+ chat_id=self.id,
+ user_id=user_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def kick_member(
+ self,
+ user_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ until_date: Union[int, datetime] = None,
+ api_kwargs: JSONDict = None,
+ revoke_messages: bool = None,
+ ) -> bool:
+ """
+ Deprecated, use :func:`~telegram.Chat.ban_member` instead.
- def get_administrators(self, *args, **kwargs):
+ .. deprecated:: 13.7
+ """
+ warnings.warn(
+ '`Chat.kick_member` is deprecated. Use `Chat.ban_member` instead.',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+
+ return self.ban_member(
+ user_id=user_id,
+ timeout=timeout,
+ until_date=until_date,
+ api_kwargs=api_kwargs,
+ revoke_messages=revoke_messages,
+ )
+
+ def ban_member(
+ self,
+ user_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ until_date: Union[int, datetime] = None,
+ api_kwargs: JSONDict = None,
+ revoke_messages: bool = None,
+ ) -> bool:
"""Shortcut for::
- bot.get_chat_administrators(update.message.chat.id, *args, **kwargs)
+ bot.ban_chat_member(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.ban_chat_member`.
Returns:
- List[:class:`telegram.ChatMember`]: A list of administrators in a chat. An Array of
- :class:`telegram.ChatMember` objects that contains information about all
- chat administrators except other bots. If the chat is a group or a supergroup
- and no administrators were appointed, only the creator will be returned
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return self.bot.ban_chat_member(
+ chat_id=self.id,
+ user_id=user_id,
+ timeout=timeout,
+ until_date=until_date,
+ api_kwargs=api_kwargs,
+ revoke_messages=revoke_messages,
+ )
+
+ def ban_sender_chat(
+ self,
+ sender_chat_id: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.ban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.ban_chat_sender_chat`.
+
+ .. versionadded:: 13.9
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
"""
- return self.bot.get_chat_administrators(self.id, *args, **kwargs)
+ return self.bot.ban_chat_sender_chat(
+ chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def ban_chat(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.ban_chat_sender_chat(sender_chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.ban_chat_sender_chat`.
+
+ .. versionadded:: 13.9
- def get_members_count(self, *args, **kwargs):
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.ban_chat_sender_chat(
+ chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def unban_sender_chat(
+ self,
+ sender_chat_id: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Shortcut for::
- bot.get_chat_members_count(update.message.chat.id, *args, **kwargs)
+ bot.unban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.unban_chat_sender_chat`.
+
+ .. versionadded:: 13.9
Returns:
- :obj:`int`
+ :obj:`bool`: On success, :obj:`True` is returned.
"""
- return self.bot.get_chat_members_count(self.id, *args, **kwargs)
+ return self.bot.unban_chat_sender_chat(
+ chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def unban_chat(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.unban_chat_sender_chat(sender_chat_id=update.effective_chat.id, *args, **kwargs)
- def get_member(self, *args, **kwargs):
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.unban_chat_sender_chat`.
+
+ .. versionadded:: 13.9
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.unban_chat_sender_chat(
+ chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def unban_member(
+ self,
+ user_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ only_if_banned: bool = None,
+ ) -> bool:
"""Shortcut for::
- bot.get_chat_member(update.message.chat.id, *args, **kwargs)
+ bot.unban_chat_member(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.unban_chat_member`.
Returns:
- :class:`telegram.ChatMember`
+ :obj:`bool`: On success, :obj:`True` is returned.
"""
- return self.bot.get_chat_member(self.id, *args, **kwargs)
+ return self.bot.unban_chat_member(
+ chat_id=self.id,
+ user_id=user_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ only_if_banned=only_if_banned,
+ )
+
+ def promote_member(
+ self,
+ user_id: Union[str, int],
+ can_change_info: bool = None,
+ can_post_messages: bool = None,
+ can_edit_messages: bool = None,
+ can_delete_messages: bool = None,
+ can_invite_users: bool = None,
+ can_restrict_members: bool = None,
+ can_pin_messages: bool = None,
+ can_promote_members: bool = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ is_anonymous: bool = None,
+ can_manage_chat: bool = None,
+ can_manage_voice_chats: bool = None,
+ ) -> bool:
+ """Shortcut for::
- def kick_member(self, *args, **kwargs):
+ bot.promote_chat_member(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.promote_chat_member`.
+
+ .. versionadded:: 13.2
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.promote_chat_member(
+ chat_id=self.id,
+ user_id=user_id,
+ can_change_info=can_change_info,
+ can_post_messages=can_post_messages,
+ can_edit_messages=can_edit_messages,
+ can_delete_messages=can_delete_messages,
+ can_invite_users=can_invite_users,
+ can_restrict_members=can_restrict_members,
+ can_pin_messages=can_pin_messages,
+ can_promote_members=can_promote_members,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ is_anonymous=is_anonymous,
+ can_manage_chat=can_manage_chat,
+ can_manage_voice_chats=can_manage_voice_chats,
+ )
+
+ def restrict_member(
+ self,
+ user_id: Union[str, int],
+ permissions: ChatPermissions,
+ until_date: Union[int, datetime] = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Shortcut for::
- bot.kick_chat_member(update.message.chat.id, *args, **kwargs)
+ bot.restrict_chat_member(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.restrict_chat_member`.
+
+ .. versionadded:: 13.2
Returns:
- :obj:`bool`: If the action was sent succesfully.
+ :obj:`bool`: On success, :obj:`True` is returned.
- Note:
- This method will only work if the `All Members Are Admins` setting is off in the
- target group. Otherwise members may only be removed by the group's creator or by the
- member that added them.
+ """
+ return self.bot.restrict_chat_member(
+ chat_id=self.id,
+ user_id=user_id,
+ permissions=permissions,
+ until_date=until_date,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def set_permissions(
+ self,
+ permissions: ChatPermissions,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.set_chat_permissions(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.set_chat_permissions`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
"""
- return self.bot.kick_chat_member(self.id, *args, **kwargs)
+ return self.bot.set_chat_permissions(
+ chat_id=self.id,
+ permissions=permissions,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def set_administrator_custom_title(
+ self,
+ user_id: Union[int, str],
+ custom_title: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
- def unban_member(self, *args, **kwargs):
+ bot.set_chat_administrator_custom_title(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.set_chat_administrator_custom_title`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.set_chat_administrator_custom_title(
+ chat_id=self.id,
+ user_id=user_id,
+ custom_title=custom_title,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def pin_message(
+ self,
+ message_id: int,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Shortcut for::
- bot.unban_chat_member(update.message.chat.id, *args, **kwargs)
+ bot.pin_chat_message(chat_id=update.effective_chat.id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.pin_chat_message`.
Returns:
- :obj:`bool`: If the action was sent successfully.
+ :obj:`bool`: On success, :obj:`True` is returned.
"""
- return self.bot.unban_chat_member(self.id, *args, **kwargs)
+ return self.bot.pin_chat_message(
+ chat_id=self.id,
+ message_id=message_id,
+ disable_notification=disable_notification,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def unpin_message(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ message_id: int = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.unpin_chat_message(chat_id=update.effective_chat.id,
+ *args,
+ **kwargs)
- def send_message(self, *args, **kwargs):
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.unpin_chat_message`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.unpin_chat_message(
+ chat_id=self.id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ message_id=message_id,
+ )
+
+ def unpin_all_messages(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Shortcut for::
- bot.send_message(Chat.id, *args, **kwargs)
+ bot.unpin_all_chat_messages(chat_id=update.effective_chat.id,
+ *args,
+ **kwargs)
- Where Chat is the current instance.
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.unpin_all_chat_messages`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.unpin_all_chat_messages(
+ chat_id=self.id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def send_message(
+ self,
+ text: str,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_message(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_message(self.id, *args, **kwargs)
+ return self.bot.send_message(
+ chat_id=self.id,
+ text=text,
+ parse_mode=parse_mode,
+ disable_web_page_preview=disable_web_page_preview,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ entities=entities,
+ protect_content=protect_content,
+ )
+
+ def send_media_group(
+ self,
+ media: List[
+ Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo']
+ ],
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> List['Message']:
+ """Shortcut for::
- def send_photo(self, *args, **kwargs):
+ bot.send_media_group(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`.
+
+ Returns:
+ List[:class:`telegram.Message`]: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_media_group(
+ chat_id=self.id,
+ media=media,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_chat_action(
+ self,
+ action: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.send_chat_action(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.send_chat_action(
+ chat_id=self.id,
+ action=action,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ send_action = send_chat_action
+ """Alias for :attr:`send_chat_action`"""
+
+ def send_photo(
+ self,
+ photo: Union[FileInput, 'PhotoSize'],
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_photo(Chat.id, *args, **kwargs)
+ bot.send_photo(update.effective_chat.id, *args, **kwargs)
- Where Chat is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_photo(self.id, *args, **kwargs)
+ return self.bot.send_photo(
+ chat_id=self.id,
+ photo=photo,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_contact(
+ self,
+ phone_number: str = None,
+ first_name: str = None,
+ last_name: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ contact: 'Contact' = None,
+ vcard: str = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_contact(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`.
- def send_audio(self, *args, **kwargs):
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_contact(
+ chat_id=self.id,
+ phone_number=phone_number,
+ first_name=first_name,
+ last_name=last_name,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ contact=contact,
+ vcard=vcard,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_audio(
+ self,
+ audio: Union[FileInput, 'Audio'],
+ duration: int = None,
+ performer: str = None,
+ title: str = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_audio(Chat.id, *args, **kwargs)
+ bot.send_audio(update.effective_chat.id, *args, **kwargs)
- Where Chat is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_audio(self.id, *args, **kwargs)
+ return self.bot.send_audio(
+ chat_id=self.id,
+ audio=audio,
+ duration=duration,
+ performer=performer,
+ title=title,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_document(
+ self,
+ document: Union[FileInput, 'Document'],
+ filename: str = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ disable_content_type_detection: bool = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_document(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
- def send_document(self, *args, **kwargs):
+ """
+ return self.bot.send_document(
+ chat_id=self.id,
+ document=document,
+ filename=filename,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ disable_content_type_detection=disable_content_type_detection,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ protect_content=protect_content,
+ )
+
+ def send_dice(
+ self,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ emoji: str = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_document(Chat.id, *args, **kwargs)
+ bot.send_dice(update.effective_chat.id, *args, **kwargs)
- Where Chat is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_document(self.id, *args, **kwargs)
+ return self.bot.send_dice(
+ chat_id=self.id,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ emoji=emoji,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_game(
+ self,
+ game_short_name: str,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
- def send_animation(self, *args, **kwargs):
+ bot.send_game(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_game(
+ chat_id=self.id,
+ game_short_name=game_short_name,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_invoice(
+ self,
+ title: str,
+ description: str,
+ payload: str,
+ provider_token: str,
+ currency: str,
+ prices: List['LabeledPrice'],
+ start_parameter: str = None,
+ photo_url: str = None,
+ photo_size: int = None,
+ photo_width: int = None,
+ photo_height: int = None,
+ need_name: bool = None,
+ need_phone_number: bool = None,
+ need_email: bool = None,
+ need_shipping_address: bool = None,
+ is_flexible: bool = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ provider_data: Union[str, object] = None,
+ send_phone_number_to_provider: bool = None,
+ send_email_to_provider: bool = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ max_tip_amount: int = None,
+ suggested_tip_amounts: List[int] = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_animation(Chat.id, *args, **kwargs)
+ bot.send_invoice(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`.
+
+ Warning:
+ As of API 5.2 :attr:`start_parameter` is an optional argument and therefore the order
+ of the arguments had to be changed. Use keyword arguments to make sure that the
+ arguments are passed correctly.
- Where Chat is the current instance.
+ .. versionchanged:: 13.5
+ As of Bot API 5.2, the parameter :attr:`start_parameter` is optional.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_animation(self.id, *args, **kwargs)
+ return self.bot.send_invoice(
+ chat_id=self.id,
+ title=title,
+ description=description,
+ payload=payload,
+ provider_token=provider_token,
+ currency=currency,
+ prices=prices,
+ start_parameter=start_parameter,
+ photo_url=photo_url,
+ photo_size=photo_size,
+ photo_width=photo_width,
+ photo_height=photo_height,
+ need_name=need_name,
+ need_phone_number=need_phone_number,
+ need_email=need_email,
+ need_shipping_address=need_shipping_address,
+ is_flexible=is_flexible,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ provider_data=provider_data,
+ send_phone_number_to_provider=send_phone_number_to_provider,
+ send_email_to_provider=send_email_to_provider,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ max_tip_amount=max_tip_amount,
+ suggested_tip_amounts=suggested_tip_amounts,
+ protect_content=protect_content,
+ )
+
+ def send_location(
+ self,
+ latitude: float = None,
+ longitude: float = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ location: 'Location' = None,
+ live_period: int = None,
+ api_kwargs: JSONDict = None,
+ horizontal_accuracy: float = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_location(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
- def send_sticker(self, *args, **kwargs):
+ """
+ return self.bot.send_location(
+ chat_id=self.id,
+ latitude=latitude,
+ longitude=longitude,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ location=location,
+ live_period=live_period,
+ api_kwargs=api_kwargs,
+ horizontal_accuracy=horizontal_accuracy,
+ heading=heading,
+ proximity_alert_radius=proximity_alert_radius,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_animation(
+ self,
+ animation: Union[FileInput, 'Animation'],
+ duration: int = None,
+ width: int = None,
+ height: int = None,
+ thumb: FileInput = None,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_sticker(Chat.id, *args, **kwargs)
+ bot.send_animation(update.effective_chat.id, *args, **kwargs)
- Where Chat is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_sticker(self.id, *args, **kwargs)
+ return self.bot.send_animation(
+ chat_id=self.id,
+ animation=animation,
+ duration=duration,
+ width=width,
+ height=height,
+ thumb=thumb,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_sticker(
+ self,
+ sticker: Union[FileInput, 'Sticker'],
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
- def send_video(self, *args, **kwargs):
+ bot.send_sticker(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_sticker(
+ chat_id=self.id,
+ sticker=sticker,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_venue(
+ self,
+ latitude: float = None,
+ longitude: float = None,
+ title: str = None,
+ address: str = None,
+ foursquare_id: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ venue: 'Venue' = None,
+ foursquare_type: str = None,
+ api_kwargs: JSONDict = None,
+ google_place_id: str = None,
+ google_place_type: str = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_video(Chat.id, *args, **kwargs)
+ bot.send_venue(update.effective_chat.id, *args, **kwargs)
- Where Chat is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_video(self.id, *args, **kwargs)
+ return self.bot.send_venue(
+ chat_id=self.id,
+ latitude=latitude,
+ longitude=longitude,
+ title=title,
+ address=address,
+ foursquare_id=foursquare_id,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ venue=venue,
+ foursquare_type=foursquare_type,
+ api_kwargs=api_kwargs,
+ google_place_id=google_place_id,
+ google_place_type=google_place_type,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_video(
+ self,
+ video: Union[FileInput, 'Video'],
+ duration: int = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ width: int = None,
+ height: int = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ supports_streaming: bool = None,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_video(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_video(
+ chat_id=self.id,
+ video=video,
+ duration=duration,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ width=width,
+ height=height,
+ parse_mode=parse_mode,
+ supports_streaming=supports_streaming,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_video_note(
+ self,
+ video_note: Union[FileInput, 'VideoNote'],
+ duration: int = None,
+ length: int = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_video_note(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_video_note(
+ chat_id=self.id,
+ video_note=video_note,
+ duration=duration,
+ length=length,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_voice(
+ self,
+ voice: Union[FileInput, 'Voice'],
+ duration: int = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
- def send_video_note(self, *args, **kwargs):
+ bot.send_voice(update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_voice(
+ chat_id=self.id,
+ voice=voice,
+ duration=duration,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_poll(
+ self,
+ question: str,
+ options: List[str],
+ is_anonymous: bool = True,
+ # We use constant.POLL_REGULAR instead of Poll.REGULAR here to avoid circular imports
+ type: str = constants.POLL_REGULAR, # pylint: disable=W0622
+ allows_multiple_answers: bool = False,
+ correct_option_id: int = None,
+ is_closed: bool = None,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ explanation: str = None,
+ explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
+ open_period: int = None,
+ close_date: Union[int, datetime] = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_video_note(Chat.id, *args, **kwargs)
+ bot.send_poll(update.effective_chat.id, *args, **kwargs)
- Where Chat is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_video_note(self.id, *args, **kwargs)
+ return self.bot.send_poll(
+ chat_id=self.id,
+ question=question,
+ options=options,
+ is_anonymous=is_anonymous,
+ type=type, # pylint=pylint,
+ allows_multiple_answers=allows_multiple_answers,
+ correct_option_id=correct_option_id,
+ is_closed=is_closed,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ explanation=explanation,
+ explanation_parse_mode=explanation_parse_mode,
+ open_period=open_period,
+ close_date=close_date,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ explanation_entities=explanation_entities,
+ protect_content=protect_content,
+ )
+
+ def send_copy(
+ self,
+ from_chat_id: Union[str, int],
+ message_id: int,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> 'MessageId':
+ """Shortcut for::
+
+ bot.copy_message(chat_id=update.effective_chat.id, *args, **kwargs)
- def send_voice(self, *args, **kwargs):
+ For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.copy_message(
+ chat_id=self.id,
+ from_chat_id=from_chat_id,
+ message_id=message_id,
+ caption=caption,
+ parse_mode=parse_mode,
+ caption_entities=caption_entities,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ allow_sending_without_reply=allow_sending_without_reply,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ def copy_message(
+ self,
+ chat_id: Union[int, str],
+ message_id: int,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> 'MessageId':
"""Shortcut for::
- bot.send_voice(Chat.id, *args, **kwargs)
+ bot.copy_message(from_chat_id=update.effective_chat.id, *args, **kwargs)
- Where Chat is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_voice(self.id, *args, **kwargs)
+ return self.bot.copy_message(
+ from_chat_id=self.id,
+ chat_id=chat_id,
+ message_id=message_id,
+ caption=caption,
+ parse_mode=parse_mode,
+ caption_entities=caption_entities,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ allow_sending_without_reply=allow_sending_without_reply,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ def export_invite_link(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> str:
+ """Shortcut for::
+
+ bot.export_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.export_chat_invite_link`.
+
+ .. versionadded:: 13.4
+
+ Returns:
+ :obj:`str`: New invite link on success.
+
+ """
+ return self.bot.export_chat_invite_link(
+ chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def create_invite_link(
+ self,
+ expire_date: Union[int, datetime] = None,
+ member_limit: int = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ name: str = None,
+ creates_join_request: bool = None,
+ ) -> 'ChatInviteLink':
+ """Shortcut for::
+
+ bot.create_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.create_chat_invite_link`.
+
+ .. versionadded:: 13.4
+
+ .. versionchanged:: 13.8
+ Edited signature according to the changes of
+ :meth:`telegram.Bot.create_chat_invite_link`.
+
+ Returns:
+ :class:`telegram.ChatInviteLink`
+
+ """
+ return self.bot.create_chat_invite_link(
+ chat_id=self.id,
+ expire_date=expire_date,
+ member_limit=member_limit,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ name=name,
+ creates_join_request=creates_join_request,
+ )
+
+ def edit_invite_link(
+ self,
+ invite_link: str,
+ expire_date: Union[int, datetime] = None,
+ member_limit: int = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ name: str = None,
+ creates_join_request: bool = None,
+ ) -> 'ChatInviteLink':
+ """Shortcut for::
+
+ bot.edit_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_chat_invite_link`.
+
+ .. versionadded:: 13.4
+
+ .. versionchanged:: 13.8
+ Edited signature according to the changes of :meth:`telegram.Bot.edit_chat_invite_link`.
+
+ Returns:
+ :class:`telegram.ChatInviteLink`
+
+ """
+ return self.bot.edit_chat_invite_link(
+ chat_id=self.id,
+ invite_link=invite_link,
+ expire_date=expire_date,
+ member_limit=member_limit,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ name=name,
+ creates_join_request=creates_join_request,
+ )
+
+ def revoke_invite_link(
+ self,
+ invite_link: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> 'ChatInviteLink':
+ """Shortcut for::
+
+ bot.revoke_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.revoke_chat_invite_link`.
+
+ .. versionadded:: 13.4
+
+ Returns:
+ :class:`telegram.ChatInviteLink`
+
+ """
+ return self.bot.revoke_chat_invite_link(
+ chat_id=self.id, invite_link=invite_link, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def approve_join_request(
+ self,
+ user_id: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.approve_chat_join_request(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.approve_chat_join_request`.
+
+ .. versionadded:: 13.8
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.approve_chat_join_request(
+ chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def decline_join_request(
+ self,
+ user_id: int,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.decline_chat_join_request(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.decline_chat_join_request`.
+
+ .. versionadded:: 13.8
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.decline_chat_join_request(
+ chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs
+ )
diff --git a/telegramer/include/telegram/chataction.py b/telegramer/include/telegram/chataction.py
index 419abac..335ab39 100644
--- a/telegramer/include/telegram/chataction.py
+++ b/telegramer/include/telegram/chataction.py
@@ -2,7 +2,7 @@
# pylint: disable=R0903
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,28 +18,57 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatAction."""
+from typing import ClassVar
+from telegram import constants
+from telegram.utils.deprecate import set_new_attribute_deprecated
-class ChatAction(object):
- """Helper class to provide constants for different chatactions."""
-
- FIND_LOCATION = 'find_location'
- """:obj:`str`: 'find_location'"""
- RECORD_AUDIO = 'record_audio'
- """:obj:`str`: 'record_audio'"""
- RECORD_VIDEO = 'record_video'
- """:obj:`str`: 'record_video'"""
- RECORD_VIDEO_NOTE = 'record_video_note'
- """:obj:`str`: 'record_video_note'"""
- TYPING = 'typing'
- """:obj:`str`: 'typing'"""
- UPLOAD_AUDIO = 'upload_audio'
- """:obj:`str`: 'upload_audio'"""
- UPLOAD_DOCUMENT = 'upload_document'
- """:obj:`str`: 'upload_document'"""
- UPLOAD_PHOTO = 'upload_photo'
- """:obj:`str`: 'upload_photo'"""
- UPLOAD_VIDEO = 'upload_video'
- """:obj:`str`: 'upload_video'"""
- UPLOAD_VIDEO_NOTE = 'upload_video_note'
- """:obj:`str`: 'upload_video_note'"""
+class ChatAction:
+ """Helper class to provide constants for different chat actions."""
+
+ __slots__ = ('__dict__',) # Adding __dict__ here since it doesn't subclass TGObject
+ FIND_LOCATION: ClassVar[str] = constants.CHATACTION_FIND_LOCATION
+ """:const:`telegram.constants.CHATACTION_FIND_LOCATION`"""
+ RECORD_AUDIO: ClassVar[str] = constants.CHATACTION_RECORD_AUDIO
+ """:const:`telegram.constants.CHATACTION_RECORD_AUDIO`
+
+ .. deprecated:: 13.5
+ Deprecated by Telegram. Use :attr:`RECORD_VOICE` instead.
+ """
+ RECORD_VOICE: ClassVar[str] = constants.CHATACTION_RECORD_VOICE
+ """:const:`telegram.constants.CHATACTION_RECORD_VOICE`
+
+ .. versionadded:: 13.5
+ """
+ RECORD_VIDEO: ClassVar[str] = constants.CHATACTION_RECORD_VIDEO
+ """:const:`telegram.constants.CHATACTION_RECORD_VIDEO`"""
+ RECORD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_RECORD_VIDEO_NOTE
+ """:const:`telegram.constants.CHATACTION_RECORD_VIDEO_NOTE`"""
+ TYPING: ClassVar[str] = constants.CHATACTION_TYPING
+ """:const:`telegram.constants.CHATACTION_TYPING`"""
+ UPLOAD_AUDIO: ClassVar[str] = constants.CHATACTION_UPLOAD_AUDIO
+ """:const:`telegram.constants.CHATACTION_UPLOAD_AUDIO`
+
+ .. deprecated:: 13.5
+ Deprecated by Telegram. Use :attr:`UPLOAD_VOICE` instead.
+ """
+ UPLOAD_VOICE: ClassVar[str] = constants.CHATACTION_UPLOAD_VOICE
+ """:const:`telegram.constants.CHATACTION_UPLOAD_VOICE`
+
+ .. versionadded:: 13.5
+ """
+ UPLOAD_DOCUMENT: ClassVar[str] = constants.CHATACTION_UPLOAD_DOCUMENT
+ """:const:`telegram.constants.CHATACTION_UPLOAD_DOCUMENT`"""
+ CHOOSE_STICKER: ClassVar[str] = constants.CHATACTION_CHOOSE_STICKER
+ """:const:`telegram.constants.CHOOSE_STICKER`
+
+ .. versionadded:: 13.8"""
+ UPLOAD_PHOTO: ClassVar[str] = constants.CHATACTION_UPLOAD_PHOTO
+ """:const:`telegram.constants.CHATACTION_UPLOAD_PHOTO`"""
+ UPLOAD_VIDEO: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO
+ """:const:`telegram.constants.CHATACTION_UPLOAD_VIDEO`"""
+ UPLOAD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO_NOTE
+ """:const:`telegram.constants.CHATACTION_UPLOAD_VIDEO_NOTE`"""
+
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
diff --git a/telegramer/include/telegram/chatinvitelink.py b/telegramer/include/telegram/chatinvitelink.py
new file mode 100644
index 0000000..6852b55
--- /dev/null
+++ b/telegramer/include/telegram/chatinvitelink.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents an invite link for a chat."""
+import datetime
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import TelegramObject, User
+from telegram.utils.helpers import from_timestamp, to_timestamp
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+
+class ChatInviteLink(TelegramObject):
+ """This object represents an invite link for a chat.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`invite_link`, :attr:`creator`, :attr:`is_primary` and
+ :attr:`is_revoked` are equal.
+
+ .. versionadded:: 13.4
+
+ Args:
+ invite_link (:obj:`str`): The invite link.
+ creator (:class:`telegram.User`): Creator of the link.
+ is_primary (:obj:`bool`): :obj:`True`, if the link is primary.
+ is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked.
+ expire_date (:class:`datetime.datetime`, optional): Date when the link will expire or
+ has been expired.
+ member_limit (:obj:`int`, optional): Maximum number of users that can be members of the
+ chat simultaneously after joining the chat via this invite link; 1-99999.
+ name (:obj:`str`, optional): Invite link name.
+
+ .. versionadded:: 13.8
+ creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat via
+ the link need to be approved by chat administrators.
+
+ .. versionadded:: 13.8
+ pending_join_request_count (:obj:`int`, optional): Number of pending join requests
+ created using this link.
+
+ .. versionadded:: 13.8
+
+ Attributes:
+ invite_link (:obj:`str`): The invite link. If the link was created by another chat
+ administrator, then the second part of the link will be replaced with ``'…'``.
+ creator (:class:`telegram.User`): Creator of the link.
+ is_primary (:obj:`bool`): :obj:`True`, if the link is primary.
+ is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked.
+ expire_date (:class:`datetime.datetime`): Optional. Date when the link will expire or
+ has been expired.
+ member_limit (:obj:`int`): Optional. Maximum number of users that can be members
+ of the chat simultaneously after joining the chat via this invite link; 1-99999.
+ name (:obj:`str`): Optional. Invite link name.
+
+ .. versionadded:: 13.8
+ creates_join_request (:obj:`bool`): Optional. :obj:`True`, if users joining the chat via
+ the link need to be approved by chat administrators.
+
+ .. versionadded:: 13.8
+ pending_join_request_count (:obj:`int`): Optional. Number of pending join requests
+ created using this link.
+
+ .. versionadded:: 13.8
+
+ """
+
+ __slots__ = (
+ 'invite_link',
+ 'creator',
+ 'is_primary',
+ 'is_revoked',
+ 'expire_date',
+ 'member_limit',
+ 'name',
+ 'creates_join_request',
+ 'pending_join_request_count',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ invite_link: str,
+ creator: User,
+ is_primary: bool,
+ is_revoked: bool,
+ expire_date: datetime.datetime = None,
+ member_limit: int = None,
+ name: str = None,
+ creates_join_request: bool = None,
+ pending_join_request_count: int = None,
+ **_kwargs: Any,
+ ):
+ # Required
+ self.invite_link = invite_link
+ self.creator = creator
+ self.is_primary = is_primary
+ self.is_revoked = is_revoked
+
+ # Optionals
+ self.expire_date = expire_date
+ self.member_limit = int(member_limit) if member_limit is not None else None
+ self.name = name
+ self.creates_join_request = creates_join_request
+ self.pending_join_request_count = (
+ int(pending_join_request_count) if pending_join_request_count is not None else None
+ )
+ self._id_attrs = (self.invite_link, self.creator, self.is_primary, self.is_revoked)
+
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatInviteLink']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ data['creator'] = User.de_json(data.get('creator'), bot)
+ data['expire_date'] = from_timestamp(data.get('expire_date', None))
+
+ return cls(**data)
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
+
+ data['expire_date'] = to_timestamp(self.expire_date)
+
+ return data
diff --git a/telegramer/include/telegram/chatjoinrequest.py b/telegramer/include/telegram/chatjoinrequest.py
new file mode 100644
index 0000000..ec89c66
--- /dev/null
+++ b/telegramer/include/telegram/chatjoinrequest.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram ChatJoinRequest."""
+import datetime
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import TelegramObject, User, Chat, ChatInviteLink
+from telegram.utils.helpers import from_timestamp, to_timestamp, DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+
+class ChatJoinRequest(TelegramObject):
+ """This object represents a join request sent to a chat.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`chat`, :attr:`from_user` and :attr:`date` are equal.
+
+ Note:
+ Since Bot API 5.5, bots are allowed to contact users who sent a join request to a chat
+ where the bot is an administrator with the
+ :attr:`~telegram.ChatMemberAdministrator.can_invite_users` administrator right – even if
+ the user never interacted with the bot before.
+
+ .. versionadded:: 13.8
+
+ Args:
+ chat (:class:`telegram.Chat`): Chat to which the request was sent.
+ from_user (:class:`telegram.User`): User that sent the join request.
+ date (:class:`datetime.datetime`): Date the request was sent.
+ bio (:obj:`str`, optional): Bio of the user.
+ invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link that was used
+ by the user to send the join request.
+ bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
+
+ Attributes:
+ chat (:class:`telegram.Chat`): Chat to which the request was sent.
+ from_user (:class:`telegram.User`): User that sent the join request.
+ date (:class:`datetime.datetime`): Date the request was sent.
+ bio (:obj:`str`): Optional. Bio of the user.
+ invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link that was used
+ by the user to send the join request.
+
+ """
+
+ __slots__ = (
+ 'chat',
+ 'from_user',
+ 'date',
+ 'bio',
+ 'invite_link',
+ 'bot',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ chat: Chat,
+ from_user: User,
+ date: datetime.datetime,
+ bio: str = None,
+ invite_link: ChatInviteLink = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
+ # Required
+ self.chat = chat
+ self.from_user = from_user
+ self.date = date
+
+ # Optionals
+ self.bio = bio
+ self.invite_link = invite_link
+
+ self.bot = bot
+ self._id_attrs = (self.chat, self.from_user, self.date)
+
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatJoinRequest']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ data['chat'] = Chat.de_json(data.get('chat'), bot)
+ data['from_user'] = User.de_json(data.get('from'), bot)
+ data['date'] = from_timestamp(data.get('date', None))
+ data['invite_link'] = ChatInviteLink.de_json(data.get('invite_link'), bot)
+
+ return cls(bot=bot, **data)
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
+
+ data['date'] = to_timestamp(self.date)
+
+ return data
+
+ def approve(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.approve_chat_join_request(chat_id=update.effective_chat.id,
+ user_id=update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.approve_chat_join_request`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.approve_chat_join_request(
+ chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def decline(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.decline_chat_join_request(chat_id=update.effective_chat.id,
+ user_id=update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.decline_chat_join_request`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.decline_chat_join_request(
+ chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs
+ )
diff --git a/telegramer/include/telegram/chatlocation.py b/telegramer/include/telegram/chatlocation.py
new file mode 100644
index 0000000..f88c708
--- /dev/null
+++ b/telegramer/include/telegram/chatlocation.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a location to which a chat is connected."""
+
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import TelegramObject
+from telegram.utils.types import JSONDict
+
+from .files.location import Location
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+
+class ChatLocation(TelegramObject):
+ """This object represents a location to which a chat is connected.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`location` is equal.
+
+ Args:
+ location (:class:`telegram.Location`): The location to which the supergroup is connected.
+ Can't be a live location.
+ address (:obj:`str`): Location address; 1-64 characters, as defined by the chat owner
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
+ Attributes:
+ location (:class:`telegram.Location`): The location to which the supergroup is connected.
+ address (:obj:`str`): Location address, as defined by the chat owner
+
+ """
+
+ __slots__ = ('location', '_id_attrs', 'address')
+
+ def __init__(
+ self,
+ location: Location,
+ address: str,
+ **_kwargs: Any,
+ ):
+ self.location = location
+ self.address = address
+
+ self._id_attrs = (self.location,)
+
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatLocation']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ data['location'] = Location.de_json(data.get('location'), bot)
+
+ return cls(bot=bot, **data)
diff --git a/telegramer/include/telegram/chatmember.py b/telegramer/include/telegram/chatmember.py
index c39df08..06968d4 100644
--- a/telegramer/include/telegram/chatmember.py
+++ b/telegramer/include/telegram/chatmember.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,104 +17,325 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatMember."""
+import datetime
+from typing import TYPE_CHECKING, Any, Optional, ClassVar, Dict, Type
-from telegram import User, TelegramObject
-from telegram.utils.helpers import to_timestamp, from_timestamp
+from telegram import TelegramObject, User, constants
+from telegram.utils.helpers import from_timestamp, to_timestamp
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class ChatMember(TelegramObject):
- """This object contains information about one member of the chat.
+ """Base class for Telegram ChatMember Objects.
+ Currently, the following 6 types of chat members are supported:
+
+ * :class:`telegram.ChatMemberOwner`
+ * :class:`telegram.ChatMemberAdministrator`
+ * :class:`telegram.ChatMemberMember`
+ * :class:`telegram.ChatMemberRestricted`
+ * :class:`telegram.ChatMemberLeft`
+ * :class:`telegram.ChatMemberBanned`
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`user` and :attr:`status` are equal.
+
+ Note:
+ As of Bot API 5.3, :class:`ChatMember` is nothing but the base class for the subclasses
+ listed above and is no longer returned directly by :meth:`~telegram.Bot.get_chat`.
+ Therefore, most of the arguments and attributes were deprecated and you should no longer
+ use :class:`ChatMember` directly.
+
+ Args:
+ user (:class:`telegram.User`): Information about the user.
+ status (:obj:`str`): The member's status in the chat. Can be
+ :attr:`~telegram.ChatMember.ADMINISTRATOR`, :attr:`~telegram.ChatMember.CREATOR`,
+ :attr:`~telegram.ChatMember.KICKED`, :attr:`~telegram.ChatMember.LEFT`,
+ :attr:`~telegram.ChatMember.MEMBER` or :attr:`~telegram.ChatMember.RESTRICTED`.
+ custom_title (:obj:`str`, optional): Owner and administrators only.
+ Custom title for this user.
+
+ .. deprecated:: 13.7
+
+ is_anonymous (:obj:`bool`, optional): Owner and administrators only. :obj:`True`, if the
+ user's presence in the chat is hidden.
+
+ .. deprecated:: 13.7
+
+ until_date (:class:`datetime.datetime`, optional): Restricted and kicked only. Date when
+ restrictions will be lifted for this user.
+
+ .. deprecated:: 13.7
+
+ can_be_edited (:obj:`bool`, optional): Administrators only. :obj:`True`, if the bot is
+ allowed to edit administrator privileges of that user.
+
+ .. deprecated:: 13.7
+
+ can_manage_chat (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
+ administrator can access the chat event log, chat statistics, message statistics in
+ channels, see channel members, see anonymous administrators in supergroups and ignore
+ slow mode. Implied by any other administrator privilege.
+
+ .. versionadded:: 13.4
+ .. deprecated:: 13.7
+
+ can_manage_voice_chats (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
+ administrator can manage voice chats.
+
+ .. versionadded:: 13.4
+ .. deprecated:: 13.7
+
+ can_change_info (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`,
+ if the user can change the chat title, photo and other settings.
+
+ .. deprecated:: 13.7
+
+ can_post_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
+ administrator can post in the channel, channels only.
+
+ .. deprecated:: 13.7
+
+ can_edit_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
+ administrator can edit messages of other users and can pin messages; channels only.
+
+ .. deprecated:: 13.7
+
+ can_delete_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
+ administrator can delete messages of other users.
+
+ .. deprecated:: 13.7
+
+ can_invite_users (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`,
+ if the user can invite new users to the chat.
+
+ .. deprecated:: 13.7
+
+ can_restrict_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
+ administrator can restrict, ban or unban chat members.
+
+ .. deprecated:: 13.7
+
+ can_pin_messages (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`,
+ if the user can pin messages, groups and supergroups only.
+
+ .. deprecated:: 13.7
+
+ can_promote_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
+ administrator can add new administrators with a subset of his own privileges or demote
+ administrators that he has promoted, directly or indirectly (promoted by administrators
+ that were appointed by the user).
+
+ .. deprecated:: 13.7
+
+ is_member (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is a member of
+ the chat at the moment of the request.
+
+ .. deprecated:: 13.7
+
+ can_send_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can
+ send text messages, contacts, locations and venues.
+
+ .. deprecated:: 13.7
+
+ can_send_media_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user
+ can send audios, documents, photos, videos, video notes and voice notes.
+
+ .. deprecated:: 13.7
+
+ can_send_polls (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is
+ allowed to send polls.
+
+ .. deprecated:: 13.7
+
+ can_send_other_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user
+ can send animations, games, stickers and use inline bots.
+
+ .. deprecated:: 13.7
+
+ can_add_web_page_previews (:obj:`bool`, optional): Restricted only. :obj:`True`, if user
+ may add web page previews to his messages.
+
+ .. deprecated:: 13.7
Attributes:
user (:class:`telegram.User`): Information about the user.
status (:obj:`str`): The member's status in the chat.
+ custom_title (:obj:`str`): Optional. Custom title for owner and administrators.
+
+ .. deprecated:: 13.7
+
+ is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's presence in the chat is
+ hidden.
+
+ .. deprecated:: 13.7
+
until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted
for this user.
+
+ .. deprecated:: 13.7
+
can_be_edited (:obj:`bool`): Optional. If the bot is allowed to edit administrator
privileges of that user.
- can_change_info (:obj:`bool`): Optional. If the administrator can change the chat title,
- photo and other settings.
+
+ .. deprecated:: 13.7
+
+ can_manage_chat (:obj:`bool`): Optional. If the administrator can access the chat event
+ log, chat statistics, message statistics in channels, see channel members, see
+ anonymous administrators in supergroups and ignore slow mode.
+
+ .. versionadded:: 13.4
+ .. deprecated:: 13.7
+
+ can_manage_voice_chats (:obj:`bool`): Optional. if the administrator can manage
+ voice chats.
+
+ .. versionadded:: 13.4
+ .. deprecated:: 13.7
+
+ can_change_info (:obj:`bool`): Optional. If the user can change the chat title, photo and
+ other settings.
+
+ .. deprecated:: 13.7
+
can_post_messages (:obj:`bool`): Optional. If the administrator can post in the channel.
+
+ .. deprecated:: 13.7
+
can_edit_messages (:obj:`bool`): Optional. If the administrator can edit messages of other
users.
+
+ .. deprecated:: 13.7
+
can_delete_messages (:obj:`bool`): Optional. If the administrator can delete messages of
other users.
- can_invite_users (:obj:`bool`): Optional. If the administrator can invite new users to the
- chat.
+
+ .. deprecated:: 13.7
+
+ can_invite_users (:obj:`bool`): Optional. If the user can invite new users to the chat.
+
+ .. deprecated:: 13.7
+
can_restrict_members (:obj:`bool`): Optional. If the administrator can restrict, ban or
unban chat members.
- can_pin_messages (:obj:`bool`): Optional. If the administrator can pin messages.
+
+ .. deprecated:: 13.7
+
+ can_pin_messages (:obj:`bool`): Optional. If the user can pin messages.
+
+ .. deprecated:: 13.7
+
can_promote_members (:obj:`bool`): Optional. If the administrator can add new
administrators.
+
+ .. deprecated:: 13.7
+
+ is_member (:obj:`bool`): Optional. Restricted only. :obj:`True`, if the user is a member of
+ the chat at the moment of the request.
+
+ .. deprecated:: 13.7
+
can_send_messages (:obj:`bool`): Optional. If the user can send text messages, contacts,
locations and venues.
+
+ .. deprecated:: 13.7
+
can_send_media_messages (:obj:`bool`): Optional. If the user can send media messages,
implies can_send_messages.
+
+ .. deprecated:: 13.7
+
+ can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to
+ send polls.
+
+ .. deprecated:: 13.7
+
can_send_other_messages (:obj:`bool`): Optional. If the user can send animations, games,
stickers and use inline bots, implies can_send_media_messages.
+
+ .. deprecated:: 13.7
+
can_add_web_page_previews (:obj:`bool`): Optional. If user may add web page previews to his
messages, implies can_send_media_messages
- Args:
- user (:class:`telegram.User`): Information about the user.
- status (:obj:`str`): The member's status in the chat. Can be 'creator', 'administrator',
- 'member', 'restricted', 'left' or 'kicked'.
- until_date (:class:`datetime.datetime`, optional): Restricted and kicked only. Date when
- restrictions will be lifted for this user.
- can_be_edited (:obj:`bool`, optional): Administrators only. True, if the bot is allowed to
- edit administrator privileges of that user.
- can_change_info (:obj:`bool`, optional): Administrators only. True, if the administrator
- can change the chat title, photo and other settings.
- can_post_messages (:obj:`bool`, optional): Administrators only. True, if the administrator
- can post in the channel, channels only.
- can_edit_messages (:obj:`bool`, optional): Administrators only. True, if the administrator
- can edit messages of other users, channels only.
- can_delete_messages (:obj:`bool`, optional): Administrators only. True, if the
- administrator can delete messages of other user.
- can_invite_users (:obj:`bool`, optional): Administrators only. True, if the administrator
- can invite new users to the chat.
- can_restrict_members (:obj:`bool`, optional): Administrators only. True, if the
- administrator can restrict, ban or unban chat members.
- can_pin_messages (:obj:`bool`, optional): Administrators only. True, if the administrator
- can pin messages, supergroups only.
- can_promote_members (:obj:`bool`, optional): Administrators only. True, if the
- administrator can add new administrators with a subset of his own privileges or demote
- administrators that he has promoted, directly or indirectly (promoted by administrators
- that were appointed by the user).
- can_send_messages (:obj:`bool`, optional): Restricted only. True, if the user can send text
- messages, contacts, locations and venues.
- can_send_media_messages (:obj:`bool`, optional): Restricted only. True, if the user can
- send audios, documents, photos, videos, video notes and voice notes, implies
- can_send_messages.
- can_send_other_messages (:obj:`bool`, optional): Restricted only. True, if the user can
- send animations, games, stickers and use inline bots, implies can_send_media_messages.
- can_add_web_page_previews (:obj:`bool`, optional): Restricted only. True, if user may add
- web page previews to his messages, implies can_send_media_messages.
+ .. deprecated:: 13.7
"""
- ADMINISTRATOR = 'administrator'
- """:obj:`str`: 'administrator'"""
- CREATOR = 'creator'
- """:obj:`str`: 'creator'"""
- KICKED = 'kicked'
- """:obj:`str`: 'kicked'"""
- LEFT = 'left'
- """:obj:`str`: 'left'"""
- MEMBER = 'member'
- """:obj:`str`: 'member'"""
- RESTRICTED = 'restricted'
- """:obj:`str`: 'restricted'"""
-
- def __init__(self, user, status, until_date=None, can_be_edited=None,
- 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, can_send_messages=None,
- can_send_media_messages=None, can_send_other_messages=None,
- can_add_web_page_previews=None, **kwargs):
+
+ __slots__ = (
+ 'is_member',
+ 'can_restrict_members',
+ 'can_delete_messages',
+ 'custom_title',
+ 'can_be_edited',
+ 'can_post_messages',
+ 'can_send_messages',
+ 'can_edit_messages',
+ 'can_send_media_messages',
+ 'is_anonymous',
+ 'can_add_web_page_previews',
+ 'can_send_other_messages',
+ 'can_invite_users',
+ 'can_send_polls',
+ 'user',
+ 'can_promote_members',
+ 'status',
+ 'can_change_info',
+ 'can_pin_messages',
+ 'can_manage_chat',
+ 'can_manage_voice_chats',
+ 'until_date',
+ '_id_attrs',
+ )
+
+ ADMINISTRATOR: ClassVar[str] = constants.CHATMEMBER_ADMINISTRATOR
+ """:const:`telegram.constants.CHATMEMBER_ADMINISTRATOR`"""
+ CREATOR: ClassVar[str] = constants.CHATMEMBER_CREATOR
+ """:const:`telegram.constants.CHATMEMBER_CREATOR`"""
+ KICKED: ClassVar[str] = constants.CHATMEMBER_KICKED
+ """:const:`telegram.constants.CHATMEMBER_KICKED`"""
+ LEFT: ClassVar[str] = constants.CHATMEMBER_LEFT
+ """:const:`telegram.constants.CHATMEMBER_LEFT`"""
+ MEMBER: ClassVar[str] = constants.CHATMEMBER_MEMBER
+ """:const:`telegram.constants.CHATMEMBER_MEMBER`"""
+ RESTRICTED: ClassVar[str] = constants.CHATMEMBER_RESTRICTED
+ """:const:`telegram.constants.CHATMEMBER_RESTRICTED`"""
+
+ def __init__(
+ self,
+ user: User,
+ status: str,
+ until_date: datetime.datetime = None,
+ can_be_edited: bool = None,
+ can_change_info: bool = None,
+ can_post_messages: bool = None,
+ can_edit_messages: bool = None,
+ can_delete_messages: bool = None,
+ can_invite_users: bool = None,
+ can_restrict_members: bool = None,
+ can_pin_messages: bool = None,
+ can_promote_members: bool = None,
+ can_send_messages: bool = None,
+ can_send_media_messages: bool = None,
+ can_send_polls: bool = None,
+ can_send_other_messages: bool = None,
+ can_add_web_page_previews: bool = None,
+ is_member: bool = None,
+ custom_title: str = None,
+ is_anonymous: bool = None,
+ can_manage_chat: bool = None,
+ can_manage_voice_chats: bool = None,
+ **_kwargs: Any,
+ ):
# Required
self.user = user
self.status = status
+
+ # Optionals
+ self.custom_title = custom_title
+ self.is_anonymous = is_anonymous
self.until_date = until_date
self.can_be_edited = can_be_edited
self.can_change_info = can_change_info
@@ -127,26 +348,368 @@ def __init__(self, user, status, until_date=None, can_be_edited=None,
self.can_promote_members = can_promote_members
self.can_send_messages = can_send_messages
self.can_send_media_messages = can_send_media_messages
+ self.can_send_polls = can_send_polls
self.can_send_other_messages = can_send_other_messages
self.can_add_web_page_previews = can_add_web_page_previews
+ self.is_member = is_member
+ self.can_manage_chat = can_manage_chat
+ self.can_manage_voice_chats = can_manage_voice_chats
self._id_attrs = (self.user, self.status)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatMember']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(ChatMember, cls).de_json(data, bot)
-
data['user'] = User.de_json(data.get('user'), bot)
data['until_date'] = from_timestamp(data.get('until_date', None))
+ _class_mapping: Dict[str, Type['ChatMember']] = {
+ cls.CREATOR: ChatMemberOwner,
+ cls.ADMINISTRATOR: ChatMemberAdministrator,
+ cls.MEMBER: ChatMemberMember,
+ cls.RESTRICTED: ChatMemberRestricted,
+ cls.LEFT: ChatMemberLeft,
+ cls.KICKED: ChatMemberBanned,
+ }
+
+ if cls is ChatMember:
+ return _class_mapping.get(data['status'], cls)(**data, bot=bot)
return cls(**data)
- def to_dict(self):
- data = super(ChatMember, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
data['until_date'] = to_timestamp(self.until_date)
return data
+
+
+class ChatMemberOwner(ChatMember):
+ """
+ Represents a chat member that owns the chat
+ and has all administrator privileges.
+
+ .. versionadded:: 13.7
+
+ Args:
+ user (:class:`telegram.User`): Information about the user.
+ custom_title (:obj:`str`, optional): Custom title for this user.
+ is_anonymous (:obj:`bool`, optional): :obj:`True`, if the
+ user's presence in the chat is hidden.
+
+ Attributes:
+ status (:obj:`str`): The member's status in the chat,
+ always :attr:`telegram.ChatMember.CREATOR`.
+ user (:class:`telegram.User`): Information about the user.
+ custom_title (:obj:`str`): Optional. Custom title for
+ this user.
+ is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's
+ presence in the chat is hidden.
+ """
+
+ __slots__ = ()
+
+ def __init__(
+ self,
+ user: User,
+ custom_title: str = None,
+ is_anonymous: bool = None,
+ **_kwargs: Any,
+ ):
+ super().__init__(
+ status=ChatMember.CREATOR,
+ user=user,
+ custom_title=custom_title,
+ is_anonymous=is_anonymous,
+ )
+
+
+class ChatMemberAdministrator(ChatMember):
+ """
+ Represents a chat member that has some additional privileges.
+
+ .. versionadded:: 13.7
+
+ Args:
+ user (:class:`telegram.User`): Information about the user.
+ can_be_edited (:obj:`bool`, optional): :obj:`True`, if the bot
+ is allowed to edit administrator privileges of that user.
+ custom_title (:obj:`str`, optional): Custom title for this user.
+ is_anonymous (:obj:`bool`, optional): :obj:`True`, if the user's
+ presence in the chat is hidden.
+ can_manage_chat (:obj:`bool`, optional): :obj:`True`, if the administrator
+ can access the chat event log, chat statistics, message statistics in
+ channels, see channel members, see anonymous administrators in supergroups
+ and ignore slow mode. Implied by any other administrator privilege.
+ can_post_messages (:obj:`bool`, optional): :obj:`True`, if the
+ administrator can post in the channel, channels only.
+ can_edit_messages (:obj:`bool`, optional): :obj:`True`, if the
+ administrator can edit messages of other users and can pin
+ messages; channels only.
+ can_delete_messages (:obj:`bool`, optional): :obj:`True`, if the
+ administrator can delete messages of other users.
+ can_manage_voice_chats (:obj:`bool`, optional): :obj:`True`, if the
+ administrator can manage voice chats.
+ can_restrict_members (:obj:`bool`, optional): :obj:`True`, if the
+ administrator can restrict, ban or unban chat members.
+ can_promote_members (:obj:`bool`, optional): :obj:`True`, if the administrator
+ can add new administrators with a subset of his own privileges or demote
+ administrators that he has promoted, directly or indirectly (promoted by
+ administrators that were appointed by the user).
+ can_change_info (:obj:`bool`, optional): :obj:`True`, if the user can change
+ the chat title, photo and other settings.
+ can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite
+ new users to the chat.
+ can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
+ to pin messages; groups and supergroups only.
+
+ Attributes:
+ status (:obj:`str`): The member's status in the chat,
+ always :attr:`telegram.ChatMember.ADMINISTRATOR`.
+ user (:class:`telegram.User`): Information about the user.
+ can_be_edited (:obj:`bool`): Optional. :obj:`True`, if the bot
+ is allowed to edit administrator privileges of that user.
+ custom_title (:obj:`str`): Optional. Custom title for this user.
+ is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's
+ presence in the chat is hidden.
+ can_manage_chat (:obj:`bool`): Optional. :obj:`True`, if the administrator
+ can access the chat event log, chat statistics, message statistics in
+ channels, see channel members, see anonymous administrators in supergroups
+ and ignore slow mode. Implied by any other administrator privilege.
+ can_post_messages (:obj:`bool`): Optional. :obj:`True`, if the
+ administrator can post in the channel, channels only.
+ can_edit_messages (:obj:`bool`): Optional. :obj:`True`, if the
+ administrator can edit messages of other users and can pin
+ messages; channels only.
+ can_delete_messages (:obj:`bool`): Optional. :obj:`True`, if the
+ administrator can delete messages of other users.
+ can_manage_voice_chats (:obj:`bool`): Optional. :obj:`True`, if the
+ administrator can manage voice chats.
+ can_restrict_members (:obj:`bool`): Optional. :obj:`True`, if the
+ administrator can restrict, ban or unban chat members.
+ can_promote_members (:obj:`bool`): Optional. :obj:`True`, if the administrator
+ can add new administrators with a subset of his own privileges or demote
+ administrators that he has promoted, directly or indirectly (promoted by
+ administrators that were appointed by the user).
+ can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user can change
+ the chat title, photo and other settings.
+ can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite
+ new users to the chat.
+ can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
+ to pin messages; groups and supergroups only.
+ """
+
+ __slots__ = ()
+
+ def __init__(
+ self,
+ user: User,
+ can_be_edited: bool = None,
+ custom_title: str = None,
+ is_anonymous: bool = None,
+ can_manage_chat: bool = None,
+ can_post_messages: bool = None,
+ can_edit_messages: bool = None,
+ can_delete_messages: bool = None,
+ can_manage_voice_chats: bool = None,
+ can_restrict_members: bool = None,
+ can_promote_members: bool = None,
+ can_change_info: bool = None,
+ can_invite_users: bool = None,
+ can_pin_messages: bool = None,
+ **_kwargs: Any,
+ ):
+ super().__init__(
+ status=ChatMember.ADMINISTRATOR,
+ user=user,
+ can_be_edited=can_be_edited,
+ custom_title=custom_title,
+ is_anonymous=is_anonymous,
+ can_manage_chat=can_manage_chat,
+ can_post_messages=can_post_messages,
+ can_edit_messages=can_edit_messages,
+ can_delete_messages=can_delete_messages,
+ can_manage_voice_chats=can_manage_voice_chats,
+ can_restrict_members=can_restrict_members,
+ can_promote_members=can_promote_members,
+ can_change_info=can_change_info,
+ can_invite_users=can_invite_users,
+ can_pin_messages=can_pin_messages,
+ )
+
+
+class ChatMemberMember(ChatMember):
+ """
+ Represents a chat member that has no additional
+ privileges or restrictions.
+
+ .. versionadded:: 13.7
+
+ Args:
+ user (:class:`telegram.User`): Information about the user.
+
+ Attributes:
+ status (:obj:`str`): The member's status in the chat,
+ always :attr:`telegram.ChatMember.MEMBER`.
+ user (:class:`telegram.User`): Information about the user.
+
+ """
+
+ __slots__ = ()
+
+ def __init__(self, user: User, **_kwargs: Any):
+ super().__init__(status=ChatMember.MEMBER, user=user)
+
+
+class ChatMemberRestricted(ChatMember):
+ """
+ Represents a chat member that is under certain restrictions
+ in the chat. Supergroups only.
+
+ .. versionadded:: 13.7
+
+ Args:
+ user (:class:`telegram.User`): Information about the user.
+ is_member (:obj:`bool`, optional): :obj:`True`, if the user is a
+ member of the chat at the moment of the request.
+ can_change_info (:obj:`bool`, optional): :obj:`True`, if the user can change
+ the chat title, photo and other settings.
+ can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite
+ new users to the chat.
+ can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
+ to pin messages; groups and supergroups only.
+ can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
+ to send text messages, contacts, locations and venues.
+ can_send_media_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
+ to send audios, documents, photos, videos, video notes and voice notes.
+ can_send_polls (:obj:`bool`, optional): :obj:`True`, if the user is allowed
+ to send polls.
+ can_send_other_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
+ to send animations, games, stickers and use inline bots.
+ can_add_web_page_previews (:obj:`bool`, optional): :obj:`True`, if the user is
+ allowed to add web page previews to their messages.
+ until_date (:class:`datetime.datetime`, optional): Date when restrictions
+ will be lifted for this user.
+
+ Attributes:
+ status (:obj:`str`): The member's status in the chat,
+ always :attr:`telegram.ChatMember.RESTRICTED`.
+ user (:class:`telegram.User`): Information about the user.
+ is_member (:obj:`bool`): Optional. :obj:`True`, if the user is a
+ member of the chat at the moment of the request.
+ can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user can change
+ the chat title, photo and other settings.
+ can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite
+ new users to the chat.
+ can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
+ to pin messages; groups and supergroups only.
+ can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
+ to send text messages, contacts, locations and venues.
+ can_send_media_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
+ to send audios, documents, photos, videos, video notes and voice notes.
+ can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
+ to send polls.
+ can_send_other_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
+ to send animations, games, stickers and use inline bots.
+ can_add_web_page_previews (:obj:`bool`): Optional. :obj:`True`, if the user is
+ allowed to add web page previews to their messages.
+ until_date (:class:`datetime.datetime`): Optional. Date when restrictions
+ will be lifted for this user.
+
+ """
+
+ __slots__ = ()
+
+ def __init__(
+ self,
+ user: User,
+ is_member: bool = None,
+ can_change_info: bool = None,
+ can_invite_users: bool = None,
+ can_pin_messages: bool = None,
+ can_send_messages: bool = None,
+ can_send_media_messages: bool = None,
+ can_send_polls: bool = None,
+ can_send_other_messages: bool = None,
+ can_add_web_page_previews: bool = None,
+ until_date: datetime.datetime = None,
+ **_kwargs: Any,
+ ):
+ super().__init__(
+ status=ChatMember.RESTRICTED,
+ user=user,
+ is_member=is_member,
+ can_change_info=can_change_info,
+ can_invite_users=can_invite_users,
+ can_pin_messages=can_pin_messages,
+ can_send_messages=can_send_messages,
+ can_send_media_messages=can_send_media_messages,
+ can_send_polls=can_send_polls,
+ can_send_other_messages=can_send_other_messages,
+ can_add_web_page_previews=can_add_web_page_previews,
+ until_date=until_date,
+ )
+
+
+class ChatMemberLeft(ChatMember):
+ """
+ Represents a chat member that isn't currently a member of the chat,
+ but may join it themselves.
+
+ .. versionadded:: 13.7
+
+ Args:
+ user (:class:`telegram.User`): Information about the user.
+
+ Attributes:
+ status (:obj:`str`): The member's status in the chat,
+ always :attr:`telegram.ChatMember.LEFT`.
+ user (:class:`telegram.User`): Information about the user.
+ """
+
+ __slots__ = ()
+
+ def __init__(self, user: User, **_kwargs: Any):
+ super().__init__(status=ChatMember.LEFT, user=user)
+
+
+class ChatMemberBanned(ChatMember):
+ """
+ Represents a chat member that was banned in the chat and
+ can't return to the chat or view chat messages.
+
+ .. versionadded:: 13.7
+
+ Args:
+ user (:class:`telegram.User`): Information about the user.
+ until_date (:class:`datetime.datetime`, optional): Date when restrictions
+ will be lifted for this user.
+
+ Attributes:
+ status (:obj:`str`): The member's status in the chat,
+ always :attr:`telegram.ChatMember.KICKED`.
+ user (:class:`telegram.User`): Information about the user.
+ until_date (:class:`datetime.datetime`): Optional. Date when restrictions
+ will be lifted for this user.
+
+ """
+
+ __slots__ = ()
+
+ def __init__(
+ self,
+ user: User,
+ until_date: datetime.datetime = None,
+ **_kwargs: Any,
+ ):
+ super().__init__(
+ status=ChatMember.KICKED,
+ user=user,
+ until_date=until_date,
+ )
diff --git a/telegramer/include/telegram/chatmemberupdated.py b/telegramer/include/telegram/chatmemberupdated.py
new file mode 100644
index 0000000..cd6c76e
--- /dev/null
+++ b/telegramer/include/telegram/chatmemberupdated.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram ChatMemberUpdated."""
+import datetime
+from typing import TYPE_CHECKING, Any, Optional, Dict, Tuple, Union
+
+from telegram import TelegramObject, User, Chat, ChatMember, ChatInviteLink
+from telegram.utils.helpers import from_timestamp, to_timestamp
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+
+class ChatMemberUpdated(TelegramObject):
+ """This object represents changes in the status of a chat member.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`chat`, :attr:`from_user`, :attr:`date`,
+ :attr:`old_chat_member` and :attr:`new_chat_member` are equal.
+
+ .. versionadded:: 13.4
+
+ Note:
+ In Python ``from`` is a reserved word, use ``from_user`` instead.
+
+ Args:
+ chat (:class:`telegram.Chat`): Chat the user belongs to.
+ from_user (:class:`telegram.User`): Performer of the action, which resulted in the change.
+ date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to
+ :class:`datetime.datetime`.
+ old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member.
+ new_chat_member (:class:`telegram.ChatMember`): New information about the chat member.
+ invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link, which was used
+ by the user to join the chat. For joining by invite link events only.
+
+ Attributes:
+ chat (:class:`telegram.Chat`): Chat the user belongs to.
+ from_user (:class:`telegram.User`): Performer of the action, which resulted in the change.
+ date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to
+ :class:`datetime.datetime`.
+ old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member.
+ new_chat_member (:class:`telegram.ChatMember`): New information about the chat member.
+ invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link, which was used
+ by the user to join the chat.
+
+ """
+
+ __slots__ = (
+ 'chat',
+ 'from_user',
+ 'date',
+ 'old_chat_member',
+ 'new_chat_member',
+ 'invite_link',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ chat: Chat,
+ from_user: User,
+ date: datetime.datetime,
+ old_chat_member: ChatMember,
+ new_chat_member: ChatMember,
+ invite_link: ChatInviteLink = None,
+ **_kwargs: Any,
+ ):
+ # Required
+ self.chat = chat
+ self.from_user = from_user
+ self.date = date
+ self.old_chat_member = old_chat_member
+ self.new_chat_member = new_chat_member
+
+ # Optionals
+ self.invite_link = invite_link
+
+ self._id_attrs = (
+ self.chat,
+ self.from_user,
+ self.date,
+ self.old_chat_member,
+ self.new_chat_member,
+ )
+
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatMemberUpdated']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ data['chat'] = Chat.de_json(data.get('chat'), bot)
+ data['from_user'] = User.de_json(data.get('from'), bot)
+ data['date'] = from_timestamp(data.get('date'))
+ data['old_chat_member'] = ChatMember.de_json(data.get('old_chat_member'), bot)
+ data['new_chat_member'] = ChatMember.de_json(data.get('new_chat_member'), bot)
+ data['invite_link'] = ChatInviteLink.de_json(data.get('invite_link'), bot)
+
+ return cls(**data)
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
+
+ # Required
+ data['date'] = to_timestamp(self.date)
+
+ return data
+
+ def difference(
+ self,
+ ) -> Dict[
+ str,
+ Tuple[
+ Union[str, bool, datetime.datetime, User], Union[str, bool, datetime.datetime, User]
+ ],
+ ]:
+ """Computes the difference between :attr:`old_chat_member` and :attr:`new_chat_member`.
+
+ Example:
+ .. code:: python
+
+ >>> chat_member_updated.difference()
+ {'custom_title': ('old title', 'new title')}
+
+ Note:
+ To determine, if the :attr:`telegram.ChatMember.user` attribute has changed, *every*
+ attribute of the user will be checked.
+
+ .. versionadded:: 13.5
+
+ Returns:
+ Dict[:obj:`str`, Tuple[:obj:`obj`, :obj:`obj`]]: A dictionary mapping attribute names
+ to tuples of the form ``(old_value, new_value)``
+ """
+ # we first get the names of the attributes that have changed
+ # user.to_dict() is unhashable, so that needs some special casing further down
+ old_dict = self.old_chat_member.to_dict()
+ old_user_dict = old_dict.pop('user')
+ new_dict = self.new_chat_member.to_dict()
+ new_user_dict = new_dict.pop('user')
+
+ # Generator for speed: we only need to iterate over it once
+ # we can't directly use the values from old_dict ^ new_dict b/c that set is unordered
+ attributes = (entry[0] for entry in set(old_dict.items()) ^ set(new_dict.items()))
+
+ result = {
+ attribute: (self.old_chat_member[attribute], self.new_chat_member[attribute])
+ for attribute in attributes
+ }
+ if old_user_dict != new_user_dict:
+ result['user'] = (self.old_chat_member.user, self.new_chat_member.user)
+
+ return result # type: ignore[return-value]
diff --git a/telegramer/include/telegram/chatpermissions.py b/telegramer/include/telegram/chatpermissions.py
new file mode 100644
index 0000000..44b989a
--- /dev/null
+++ b/telegramer/include/telegram/chatpermissions.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram ChatPermission."""
+
+from typing import Any
+
+from telegram import TelegramObject
+
+
+class ChatPermissions(TelegramObject):
+ """Describes actions that a non-administrator user is allowed to take in a chat.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`can_send_messages`, :attr:`can_send_media_messages`,
+ :attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`,
+ :attr:`can_change_info`, :attr:`can_invite_users` and :attr:`can_pin_messages` are equal.
+
+ Note:
+ Though not stated explicitly in the official docs, Telegram changes not only the
+ permissions that are set, but also sets all the others to :obj:`False`. However, since not
+ documented, this behaviour may change unbeknown to PTB.
+
+ Args:
+ can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send text
+ messages, contacts, locations and venues.
+ can_send_media_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to
+ send audios, documents, photos, videos, video notes and voice notes, implies
+ :attr:`can_send_messages`.
+ can_send_polls (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send polls,
+ implies :attr:`can_send_messages`.
+ can_send_other_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to
+ send animations, games, stickers and use inline bots, implies
+ :attr:`can_send_media_messages`.
+ can_add_web_page_previews (:obj:`bool`, optional): :obj:`True`, if the user is allowed to
+ add web page previews to their messages, implies :attr:`can_send_media_messages`.
+ can_change_info (:obj:`bool`, optional): :obj:`True`, if the user is allowed to change the
+ chat title, photo and other settings. Ignored in public supergroups.
+ can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user is allowed to invite new
+ users to the chat.
+ can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin
+ messages. Ignored in public supergroups.
+
+ Attributes:
+ can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text
+ messages, contacts, locations and venues.
+ can_send_media_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to
+ send audios, documents, photos, videos, video notes and voice notes, implies
+ :attr:`can_send_messages`.
+ can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send polls,
+ implies :attr:`can_send_messages`.
+ can_send_other_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to
+ send animations, games, stickers and use inline bots, implies
+ :attr:`can_send_media_messages`.
+ can_add_web_page_previews (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to
+ add web page previews to their messages, implies :attr:`can_send_media_messages`.
+ can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to change the
+ chat title, photo and other settings. Ignored in public supergroups.
+ can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to invite
+ new users to the chat.
+ can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin
+ messages. Ignored in public supergroups.
+
+ """
+
+ __slots__ = (
+ 'can_send_other_messages',
+ 'can_invite_users',
+ 'can_send_polls',
+ '_id_attrs',
+ 'can_send_messages',
+ 'can_send_media_messages',
+ 'can_change_info',
+ 'can_pin_messages',
+ 'can_add_web_page_previews',
+ )
+
+ def __init__(
+ self,
+ can_send_messages: bool = None,
+ can_send_media_messages: bool = None,
+ can_send_polls: bool = None,
+ can_send_other_messages: bool = None,
+ can_add_web_page_previews: bool = None,
+ can_change_info: bool = None,
+ can_invite_users: bool = None,
+ can_pin_messages: bool = None,
+ **_kwargs: Any,
+ ):
+ # Required
+ self.can_send_messages = can_send_messages
+ self.can_send_media_messages = can_send_media_messages
+ self.can_send_polls = can_send_polls
+ self.can_send_other_messages = can_send_other_messages
+ self.can_add_web_page_previews = can_add_web_page_previews
+ self.can_change_info = can_change_info
+ self.can_invite_users = can_invite_users
+ self.can_pin_messages = can_pin_messages
+
+ self._id_attrs = (
+ self.can_send_messages,
+ self.can_send_media_messages,
+ self.can_send_polls,
+ self.can_send_other_messages,
+ self.can_add_web_page_previews,
+ self.can_change_info,
+ self.can_invite_users,
+ self.can_pin_messages,
+ )
diff --git a/telegramer/include/telegram/choseninlineresult.py b/telegramer/include/telegram/choseninlineresult.py
index f25f913..b931553 100644
--- a/telegramer/include/telegram/choseninlineresult.py
+++ b/telegramer/include/telegram/choseninlineresult.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python
-# pylint: disable=R0902,R0912,R0913
+# pylint: disable=R0902,R0913
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,7 +19,13 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChosenInlineResult."""
-from telegram import TelegramObject, User, Location
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import Location, TelegramObject, User
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class ChosenInlineResult(TelegramObject):
@@ -27,15 +33,13 @@ class ChosenInlineResult(TelegramObject):
Represents a result of an inline query that was chosen by the user and sent to their chat
partner.
- Note:
- In Python `from` is a reserved word, use `from_user` instead.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`result_id` is equal.
- Attributes:
- result_id (:obj:`str`): The unique identifier for the result that was chosen.
- from_user (:class:`telegram.User`): The user that chose the result.
- location (:class:`telegram.Location`): Optional. Sender location.
- inline_message_id (:obj:`str`): Optional. Identifier of the sent inline message.
- query (:obj:`str`): The query that was used to obtain the result.
+ Note:
+ * In Python ``from`` is a reserved word, use ``from_user`` instead.
+ * It is necessary to enable inline feedback via `@Botfather `_ in
+ order to receive these objects in updates.
Args:
result_id (:obj:`str`): The unique identifier for the result that was chosen.
@@ -48,15 +52,26 @@ class ChosenInlineResult(TelegramObject):
query (:obj:`str`): The query that was used to obtain the result.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ result_id (:obj:`str`): The unique identifier for the result that was chosen.
+ from_user (:class:`telegram.User`): The user that chose the result.
+ location (:class:`telegram.Location`): Optional. Sender location.
+ inline_message_id (:obj:`str`): Optional. Identifier of the sent inline message.
+ query (:obj:`str`): The query that was used to obtain the result.
+
"""
- def __init__(self,
- result_id,
- from_user,
- query,
- location=None,
- inline_message_id=None,
- **kwargs):
+ __slots__ = ('location', 'result_id', 'from_user', 'inline_message_id', '_id_attrs', 'query')
+
+ def __init__(
+ self,
+ result_id: str,
+ from_user: User,
+ query: str,
+ location: Location = None,
+ inline_message_id: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.result_id = result_id
self.from_user = from_user
@@ -68,11 +83,13 @@ def __init__(self,
self._id_attrs = (self.result_id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChosenInlineResult']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(ChosenInlineResult, cls).de_json(data, bot)
# Required
data['from_user'] = User.de_json(data.pop('from'), bot)
# Optionals
diff --git a/telegramer/include/telegram/constants.py b/telegramer/include/telegram/constants.py
index d386382..b7600a7 100644
--- a/telegramer/include/telegram/constants.py
+++ b/telegramer/include/telegram/constants.py
@@ -1,5 +1,5 @@
# python-telegram-bot - a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# by the python-telegram-bot contributors
#
# This program is free software: you can redistribute it and/or modify
@@ -17,38 +17,382 @@
"""Constants in the Telegram network.
The following constants were extracted from the
-`Telegram Bots FAQ `_.
+`Telegram Bots FAQ `_ and
+`Telegram Bots API `_.
Attributes:
+ BOT_API_VERSION (:obj:`str`): `5.7`. Telegram Bot API version supported by this
+ version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``.
+
+ .. versionadded:: 13.4
MAX_MESSAGE_LENGTH (:obj:`int`): 4096
- MAX_CAPTION_LENGTH (:obj:`int`): 200
+ MAX_CAPTION_LENGTH (:obj:`int`): 1024
SUPPORTED_WEBHOOK_PORTS (List[:obj:`int`]): [443, 80, 88, 8443]
MAX_FILESIZE_DOWNLOAD (:obj:`int`): In bytes (20MB)
MAX_FILESIZE_UPLOAD (:obj:`int`): In bytes (50MB)
+ MAX_PHOTOSIZE_UPLOAD (:obj:`int`): In bytes (10MB)
MAX_MESSAGES_PER_SECOND_PER_CHAT (:obj:`int`): `1`. Telegram may allow short bursts that go
over this limit, but eventually you'll begin receiving 429 errors.
MAX_MESSAGES_PER_SECOND (:obj:`int`): 30
MAX_MESSAGES_PER_MINUTE_PER_GROUP (:obj:`int`): 20
MAX_INLINE_QUERY_RESULTS (:obj:`int`): 50
+ MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH (:obj:`int`): 200
+
+ .. versionadded:: 13.2
The following constant have been found by experimentation:
Attributes:
MAX_MESSAGE_ENTITIES (:obj:`int`): 100 (Beyond this cap telegram will simply ignore further
formatting styles)
+ ANONYMOUS_ADMIN_ID (:obj:`int`): ``1087968824`` (User id in groups for anonymous admin)
+ SERVICE_CHAT_ID (:obj:`int`): ``777000`` (Telegram service chat, that also acts as sender of
+ channel posts forwarded to discussion groups)
+ FAKE_CHANNEL_ID (:obj:`int`): ``136817688`` (User id in groups when message is sent on behalf
+ of a channel).
+
+ .. versionadded:: 13.9
+
+The following constants are related to specific classes and are also available
+as attributes of those classes:
+
+:class:`telegram.Chat`:
+
+Attributes:
+ CHAT_PRIVATE (:obj:`str`): ``'private'``
+ CHAT_GROUP (:obj:`str`): ``'group'``
+ CHAT_SUPERGROUP (:obj:`str`): ``'supergroup'``
+ CHAT_CHANNEL (:obj:`str`): ``'channel'``
+ CHAT_SENDER (:obj:`str`): ``'sender'``. Only relevant for
+ :attr:`telegram.InlineQuery.chat_type`.
+
+ .. versionadded:: 13.5
+
+:class:`telegram.ChatAction`:
+
+Attributes:
+ CHATACTION_FIND_LOCATION (:obj:`str`): ``'find_location'``
+ CHATACTION_RECORD_AUDIO (:obj:`str`): ``'record_audio'``
+
+ .. deprecated:: 13.5
+ Deprecated by Telegram. Use :const:`CHATACTION_RECORD_VOICE` instead.
+ CHATACTION_RECORD_VOICE (:obj:`str`): ``'record_voice'``
+
+ .. versionadded:: 13.5
+ CHATACTION_RECORD_VIDEO (:obj:`str`): ``'record_video'``
+ CHATACTION_RECORD_VIDEO_NOTE (:obj:`str`): ``'record_video_note'``
+ CHATACTION_TYPING (:obj:`str`): ``'typing'``
+ CHATACTION_UPLOAD_AUDIO (:obj:`str`): ``'upload_audio'``
+
+ .. deprecated:: 13.5
+ Deprecated by Telegram. Use :const:`CHATACTION_UPLOAD_VOICE` instead.
+ CHATACTION_UPLOAD_VOICE (:obj:`str`): ``'upload_voice'``
+
+ .. versionadded:: 13.5
+ CHATACTION_UPLOAD_DOCUMENT (:obj:`str`): ``'upload_document'``
+ CHATACTION_CHOOSE_STICKER (:obj:`str`): ``'choose_sticker'``
+
+ .. versionadded:: 13.8
+ CHATACTION_UPLOAD_PHOTO (:obj:`str`): ``'upload_photo'``
+ CHATACTION_UPLOAD_VIDEO (:obj:`str`): ``'upload_video'``
+ CHATACTION_UPLOAD_VIDEO_NOTE (:obj:`str`): ``'upload_video_note'``
+
+:class:`telegram.ChatMember`:
+
+Attributes:
+ CHATMEMBER_ADMINISTRATOR (:obj:`str`): ``'administrator'``
+ CHATMEMBER_CREATOR (:obj:`str`): ``'creator'``
+ CHATMEMBER_KICKED (:obj:`str`): ``'kicked'``
+ CHATMEMBER_LEFT (:obj:`str`): ``'left'``
+ CHATMEMBER_MEMBER (:obj:`str`): ``'member'``
+ CHATMEMBER_RESTRICTED (:obj:`str`): ``'restricted'``
+
+:class:`telegram.Dice`:
+
+Attributes:
+ DICE_DICE (:obj:`str`): ``'🎲'``
+ DICE_DARTS (:obj:`str`): ``'🎯'``
+ DICE_BASKETBALL (:obj:`str`): ``'🏀'``
+ DICE_FOOTBALL (:obj:`str`): ``'⚽'``
+ DICE_SLOT_MACHINE (:obj:`str`): ``'🎰'``
+ DICE_BOWLING (:obj:`str`): ``'🎳'``
+
+ .. versionadded:: 13.4
+ DICE_ALL_EMOJI (List[:obj:`str`]): List of all supported base emoji.
+
+ .. versionchanged:: 13.4
+ Added :attr:`DICE_BOWLING`
+
+:class:`telegram.MessageEntity`:
+
+Attributes:
+ MESSAGEENTITY_MENTION (:obj:`str`): ``'mention'``
+ MESSAGEENTITY_HASHTAG (:obj:`str`): ``'hashtag'``
+ MESSAGEENTITY_CASHTAG (:obj:`str`): ``'cashtag'``
+ MESSAGEENTITY_PHONE_NUMBER (:obj:`str`): ``'phone_number'``
+ MESSAGEENTITY_BOT_COMMAND (:obj:`str`): ``'bot_command'``
+ MESSAGEENTITY_URL (:obj:`str`): ``'url'``
+ MESSAGEENTITY_EMAIL (:obj:`str`): ``'email'``
+ MESSAGEENTITY_BOLD (:obj:`str`): ``'bold'``
+ MESSAGEENTITY_ITALIC (:obj:`str`): ``'italic'``
+ MESSAGEENTITY_CODE (:obj:`str`): ``'code'``
+ MESSAGEENTITY_PRE (:obj:`str`): ``'pre'``
+ MESSAGEENTITY_TEXT_LINK (:obj:`str`): ``'text_link'``
+ MESSAGEENTITY_TEXT_MENTION (:obj:`str`): ``'text_mention'``
+ MESSAGEENTITY_UNDERLINE (:obj:`str`): ``'underline'``
+ MESSAGEENTITY_STRIKETHROUGH (:obj:`str`): ``'strikethrough'``
+ MESSAGEENTITY_SPOILER (:obj:`str`): ``'spoiler'``
+
+ .. versionadded:: 13.10
+ MESSAGEENTITY_ALL_TYPES (List[:obj:`str`]): List of all the types of message entity.
+
+:class:`telegram.ParseMode`:
+
+Attributes:
+ PARSEMODE_MARKDOWN (:obj:`str`): ``'Markdown'``
+ PARSEMODE_MARKDOWN_V2 (:obj:`str`): ``'MarkdownV2'``
+ PARSEMODE_HTML (:obj:`str`): ``'HTML'``
+
+:class:`telegram.Poll`:
+
+Attributes:
+ POLL_REGULAR (:obj:`str`): ``'regular'``
+ POLL_QUIZ (:obj:`str`): ``'quiz'``
+ MAX_POLL_QUESTION_LENGTH (:obj:`int`): 300
+ MAX_POLL_OPTION_LENGTH (:obj:`int`): 100
+
+:class:`telegram.MaskPosition`:
+
+Attributes:
+ STICKER_FOREHEAD (:obj:`str`): ``'forehead'``
+ STICKER_EYES (:obj:`str`): ``'eyes'``
+ STICKER_MOUTH (:obj:`str`): ``'mouth'``
+ STICKER_CHIN (:obj:`str`): ``'chin'``
+
+:class:`telegram.Update`:
+
+Attributes:
+ UPDATE_MESSAGE (:obj:`str`): ``'message'``
+
+ .. versionadded:: 13.5
+ UPDATE_EDITED_MESSAGE (:obj:`str`): ``'edited_message'``
+
+ .. versionadded:: 13.5
+ UPDATE_CHANNEL_POST (:obj:`str`): ``'channel_post'``
+
+ .. versionadded:: 13.5
+ UPDATE_EDITED_CHANNEL_POST (:obj:`str`): ``'edited_channel_post'``
+
+ .. versionadded:: 13.5
+ UPDATE_INLINE_QUERY (:obj:`str`): ``'inline_query'``
+
+ .. versionadded:: 13.5
+ UPDATE_CHOSEN_INLINE_RESULT (:obj:`str`): ``'chosen_inline_result'``
+
+ .. versionadded:: 13.5
+ UPDATE_CALLBACK_QUERY (:obj:`str`): ``'callback_query'``
+
+ .. versionadded:: 13.5
+ UPDATE_SHIPPING_QUERY (:obj:`str`): ``'shipping_query'``
+
+ .. versionadded:: 13.5
+ UPDATE_PRE_CHECKOUT_QUERY (:obj:`str`): ``'pre_checkout_query'``
+
+ .. versionadded:: 13.5
+ UPDATE_POLL (:obj:`str`): ``'poll'``
+
+ .. versionadded:: 13.5
+ UPDATE_POLL_ANSWER (:obj:`str`): ``'poll_answer'``
+
+ .. versionadded:: 13.5
+ UPDATE_MY_CHAT_MEMBER (:obj:`str`): ``'my_chat_member'``
+
+ .. versionadded:: 13.5
+ UPDATE_CHAT_MEMBER (:obj:`str`): ``'chat_member'``
+
+ .. versionadded:: 13.5
+ UPDATE_CHAT_JOIN_REQUEST (:obj:`str`): ``'chat_join_request'``
+
+ .. versionadded:: 13.8
+ UPDATE_ALL_TYPES (List[:obj:`str`]): List of all update types.
+
+ .. versionadded:: 13.5
+ .. versionchanged:: 13.8
+
+:class:`telegram.BotCommandScope`:
+
+Attributes:
+ BOT_COMMAND_SCOPE_DEFAULT (:obj:`str`): ``'default'``
+
+ ..versionadded:: 13.7
+ BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS (:obj:`str`): ``'all_private_chats'``
+
+ ..versionadded:: 13.7
+ BOT_COMMAND_SCOPE_ALL_GROUP_CHATS (:obj:`str`): ``'all_group_chats'``
+
+ ..versionadded:: 13.7
+ BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS (:obj:`str`): ``'all_chat_administrators'``
+
+ ..versionadded:: 13.7
+ BOT_COMMAND_SCOPE_CHAT (:obj:`str`): ``'chat'``
+
+ ..versionadded:: 13.7
+ BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS (:obj:`str`): ``'chat_administrators'``
+
+ ..versionadded:: 13.7
+ BOT_COMMAND_SCOPE_CHAT_MEMBER (:obj:`str`): ``'chat_member'``
+
+ ..versionadded:: 13.7
"""
+from typing import List
-MAX_MESSAGE_LENGTH = 4096
-MAX_CAPTION_LENGTH = 200
+BOT_API_VERSION: str = '5.7'
+MAX_MESSAGE_LENGTH: int = 4096
+MAX_CAPTION_LENGTH: int = 1024
+ANONYMOUS_ADMIN_ID: int = 1087968824
+SERVICE_CHAT_ID: int = 777000
+FAKE_CHANNEL_ID: int = 136817688
# constants above this line are tested
-SUPPORTED_WEBHOOK_PORTS = [443, 80, 88, 8443]
-MAX_FILESIZE_DOWNLOAD = int(20E6) # (20MB)
-MAX_FILESIZE_UPLOAD = int(50E6) # (50MB)
-MAX_MESSAGES_PER_SECOND_PER_CHAT = 1
-MAX_MESSAGES_PER_SECOND = 30
-MAX_MESSAGES_PER_MINUTE_PER_GROUP = 20
-MAX_MESSAGE_ENTITIES = 100
-MAX_INLINE_QUERY_RESULTS = 50
+SUPPORTED_WEBHOOK_PORTS: List[int] = [443, 80, 88, 8443]
+MAX_FILESIZE_DOWNLOAD: int = int(20e6) # (20MB)
+MAX_FILESIZE_UPLOAD: int = int(50e6) # (50MB)
+MAX_PHOTOSIZE_UPLOAD: int = int(10e6) # (10MB)
+MAX_MESSAGES_PER_SECOND_PER_CHAT: int = 1
+MAX_MESSAGES_PER_SECOND: int = 30
+MAX_MESSAGES_PER_MINUTE_PER_GROUP: int = 20
+MAX_MESSAGE_ENTITIES: int = 100
+MAX_INLINE_QUERY_RESULTS: int = 50
+MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH: int = 200
+
+CHAT_SENDER: str = 'sender'
+CHAT_PRIVATE: str = 'private'
+CHAT_GROUP: str = 'group'
+CHAT_SUPERGROUP: str = 'supergroup'
+CHAT_CHANNEL: str = 'channel'
+
+CHATACTION_FIND_LOCATION: str = 'find_location'
+CHATACTION_RECORD_AUDIO: str = 'record_audio'
+CHATACTION_RECORD_VOICE: str = 'record_voice'
+CHATACTION_RECORD_VIDEO: str = 'record_video'
+CHATACTION_RECORD_VIDEO_NOTE: str = 'record_video_note'
+CHATACTION_TYPING: str = 'typing'
+CHATACTION_UPLOAD_AUDIO: str = 'upload_audio'
+CHATACTION_UPLOAD_VOICE: str = 'upload_voice'
+CHATACTION_UPLOAD_DOCUMENT: str = 'upload_document'
+CHATACTION_CHOOSE_STICKER: str = 'choose_sticker'
+CHATACTION_UPLOAD_PHOTO: str = 'upload_photo'
+CHATACTION_UPLOAD_VIDEO: str = 'upload_video'
+CHATACTION_UPLOAD_VIDEO_NOTE: str = 'upload_video_note'
+
+CHATMEMBER_ADMINISTRATOR: str = 'administrator'
+CHATMEMBER_CREATOR: str = 'creator'
+CHATMEMBER_KICKED: str = 'kicked'
+CHATMEMBER_LEFT: str = 'left'
+CHATMEMBER_MEMBER: str = 'member'
+CHATMEMBER_RESTRICTED: str = 'restricted'
+
+DICE_DICE: str = '🎲'
+DICE_DARTS: str = '🎯'
+DICE_BASKETBALL: str = '🏀'
+DICE_FOOTBALL: str = '⚽'
+DICE_SLOT_MACHINE: str = '🎰'
+DICE_BOWLING: str = '🎳'
+DICE_ALL_EMOJI: List[str] = [
+ DICE_DICE,
+ DICE_DARTS,
+ DICE_BASKETBALL,
+ DICE_FOOTBALL,
+ DICE_SLOT_MACHINE,
+ DICE_BOWLING,
+]
+
+MESSAGEENTITY_MENTION: str = 'mention'
+MESSAGEENTITY_HASHTAG: str = 'hashtag'
+MESSAGEENTITY_CASHTAG: str = 'cashtag'
+MESSAGEENTITY_PHONE_NUMBER: str = 'phone_number'
+MESSAGEENTITY_BOT_COMMAND: str = 'bot_command'
+MESSAGEENTITY_URL: str = 'url'
+MESSAGEENTITY_EMAIL: str = 'email'
+MESSAGEENTITY_BOLD: str = 'bold'
+MESSAGEENTITY_ITALIC: str = 'italic'
+MESSAGEENTITY_CODE: str = 'code'
+MESSAGEENTITY_PRE: str = 'pre'
+MESSAGEENTITY_TEXT_LINK: str = 'text_link'
+MESSAGEENTITY_TEXT_MENTION: str = 'text_mention'
+MESSAGEENTITY_UNDERLINE: str = 'underline'
+MESSAGEENTITY_STRIKETHROUGH: str = 'strikethrough'
+MESSAGEENTITY_SPOILER: str = 'spoiler'
+MESSAGEENTITY_ALL_TYPES: List[str] = [
+ MESSAGEENTITY_MENTION,
+ MESSAGEENTITY_HASHTAG,
+ MESSAGEENTITY_CASHTAG,
+ MESSAGEENTITY_PHONE_NUMBER,
+ MESSAGEENTITY_BOT_COMMAND,
+ MESSAGEENTITY_URL,
+ MESSAGEENTITY_EMAIL,
+ MESSAGEENTITY_BOLD,
+ MESSAGEENTITY_ITALIC,
+ MESSAGEENTITY_CODE,
+ MESSAGEENTITY_PRE,
+ MESSAGEENTITY_TEXT_LINK,
+ MESSAGEENTITY_TEXT_MENTION,
+ MESSAGEENTITY_UNDERLINE,
+ MESSAGEENTITY_STRIKETHROUGH,
+ MESSAGEENTITY_SPOILER,
+]
+
+PARSEMODE_MARKDOWN: str = 'Markdown'
+PARSEMODE_MARKDOWN_V2: str = 'MarkdownV2'
+PARSEMODE_HTML: str = 'HTML'
+
+POLL_REGULAR: str = 'regular'
+POLL_QUIZ: str = 'quiz'
+MAX_POLL_QUESTION_LENGTH: int = 300
+MAX_POLL_OPTION_LENGTH: int = 100
+
+STICKER_FOREHEAD: str = 'forehead'
+STICKER_EYES: str = 'eyes'
+STICKER_MOUTH: str = 'mouth'
+STICKER_CHIN: str = 'chin'
+
+UPDATE_MESSAGE = 'message'
+UPDATE_EDITED_MESSAGE = 'edited_message'
+UPDATE_CHANNEL_POST = 'channel_post'
+UPDATE_EDITED_CHANNEL_POST = 'edited_channel_post'
+UPDATE_INLINE_QUERY = 'inline_query'
+UPDATE_CHOSEN_INLINE_RESULT = 'chosen_inline_result'
+UPDATE_CALLBACK_QUERY = 'callback_query'
+UPDATE_SHIPPING_QUERY = 'shipping_query'
+UPDATE_PRE_CHECKOUT_QUERY = 'pre_checkout_query'
+UPDATE_POLL = 'poll'
+UPDATE_POLL_ANSWER = 'poll_answer'
+UPDATE_MY_CHAT_MEMBER = 'my_chat_member'
+UPDATE_CHAT_MEMBER = 'chat_member'
+UPDATE_CHAT_JOIN_REQUEST = 'chat_join_request'
+UPDATE_ALL_TYPES = [
+ UPDATE_MESSAGE,
+ UPDATE_EDITED_MESSAGE,
+ UPDATE_CHANNEL_POST,
+ UPDATE_EDITED_CHANNEL_POST,
+ UPDATE_INLINE_QUERY,
+ UPDATE_CHOSEN_INLINE_RESULT,
+ UPDATE_CALLBACK_QUERY,
+ UPDATE_SHIPPING_QUERY,
+ UPDATE_PRE_CHECKOUT_QUERY,
+ UPDATE_POLL,
+ UPDATE_POLL_ANSWER,
+ UPDATE_MY_CHAT_MEMBER,
+ UPDATE_CHAT_MEMBER,
+ UPDATE_CHAT_JOIN_REQUEST,
+]
+
+BOT_COMMAND_SCOPE_DEFAULT = 'default'
+BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS = 'all_private_chats'
+BOT_COMMAND_SCOPE_ALL_GROUP_CHATS = 'all_group_chats'
+BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS = 'all_chat_administrators'
+BOT_COMMAND_SCOPE_CHAT = 'chat'
+BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS = 'chat_administrators'
+BOT_COMMAND_SCOPE_CHAT_MEMBER = 'chat_member'
diff --git a/telegramer/include/telegram/dice.py b/telegramer/include/telegram/dice.py
new file mode 100644
index 0000000..8836529
--- /dev/null
+++ b/telegramer/include/telegram/dice.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+# pylint: disable=R0903
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram Dice."""
+from typing import Any, List, ClassVar
+
+from telegram import TelegramObject, constants
+
+
+class Dice(TelegramObject):
+ """
+ This object represents an animated emoji with a random value for currently supported base
+ emoji. (The singular form of "dice" is "die". However, PTB mimics the Telegram API, which uses
+ the term "dice".)
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`value` and :attr:`emoji` are equal.
+
+ Note:
+ If :attr:`emoji` is "🎯", a value of 6 currently represents a bullseye, while a value of 1
+ indicates that the dartboard was missed. However, this behaviour is undocumented and might
+ be changed by Telegram.
+
+ If :attr:`emoji` is "🏀", a value of 4 or 5 currently score a basket, while a value of 1 to
+ 3 indicates that the basket was missed. However, this behaviour is undocumented and might
+ be changed by Telegram.
+
+ If :attr:`emoji` is "⚽", a value of 4 to 5 currently scores a goal, while a value of 1 to
+ 3 indicates that the goal was missed. However, this behaviour is undocumented and might
+ be changed by Telegram.
+
+ If :attr:`emoji` is "🎳", a value of 6 knocks all the pins, while a value of 1 means all
+ the pins were missed. However, this behaviour is undocumented and might be changed by
+ Telegram.
+
+ If :attr:`emoji` is "🎰", each value corresponds to a unique combination of symbols, which
+ can be found at our `wiki `_. However, this behaviour is undocumented
+ and might be changed by Telegram.
+
+ Args:
+ value (:obj:`int`): Value of the dice. 1-6 for dice, darts and bowling balls, 1-5 for
+ basketball and football/soccer ball, 1-64 for slot machine.
+ emoji (:obj:`str`): Emoji on which the dice throw animation is based.
+
+ Attributes:
+ value (:obj:`int`): Value of the dice.
+ emoji (:obj:`str`): Emoji on which the dice throw animation is based.
+
+ """
+
+ __slots__ = ('emoji', 'value', '_id_attrs')
+
+ def __init__(self, value: int, emoji: str, **_kwargs: Any):
+ self.value = value
+ self.emoji = emoji
+
+ self._id_attrs = (self.value, self.emoji)
+
+ DICE: ClassVar[str] = constants.DICE_DICE # skipcq: PTC-W0052
+ """:const:`telegram.constants.DICE_DICE`"""
+ DARTS: ClassVar[str] = constants.DICE_DARTS
+ """:const:`telegram.constants.DICE_DARTS`"""
+ BASKETBALL: ClassVar[str] = constants.DICE_BASKETBALL
+ """:const:`telegram.constants.DICE_BASKETBALL`"""
+ FOOTBALL: ClassVar[str] = constants.DICE_FOOTBALL
+ """:const:`telegram.constants.DICE_FOOTBALL`"""
+ SLOT_MACHINE: ClassVar[str] = constants.DICE_SLOT_MACHINE
+ """:const:`telegram.constants.DICE_SLOT_MACHINE`"""
+ BOWLING: ClassVar[str] = constants.DICE_BOWLING
+ """
+ :const:`telegram.constants.DICE_BOWLING`
+
+ .. versionadded:: 13.4
+ """
+ ALL_EMOJI: ClassVar[List[str]] = constants.DICE_ALL_EMOJI
+ """:const:`telegram.constants.DICE_ALL_EMOJI`"""
diff --git a/telegramer/include/telegram/error.py b/telegramer/include/telegram/error.py
index 1ea6ec1..3cf41d5 100644
--- a/telegramer/include/telegram/error.py
+++ b/telegramer/include/telegram/error.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,29 +16,36 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=C0115
"""This module contains an object that represents Telegram errors."""
+from typing import Tuple
-def _lstrip_str(in_s, lstr):
+def _lstrip_str(in_s: str, lstr: str) -> str:
"""
Args:
in_s (:obj:`str`): in string
lstr (:obj:`str`): substr to strip from left side
Returns:
- str:
+ :obj:`str`: The stripped string.
"""
if in_s.startswith(lstr):
- res = in_s[len(lstr):]
+ res = in_s[len(lstr) :]
else:
res = in_s
return res
class TelegramError(Exception):
- def __init__(self, message):
- super(TelegramError, self).__init__()
+ """Base class for Telegram errors."""
+
+ # Apparently the base class Exception already has __dict__ in it, so its not included here
+ __slots__ = ('message',)
+
+ def __init__(self, message: str):
+ super().__init__()
msg = _lstrip_str(message, 'Error: ')
msg = _lstrip_str(msg, '[Error]: ')
@@ -48,55 +55,97 @@ def __init__(self, message):
msg = msg.capitalize()
self.message = msg
- def __str__(self):
- return '%s' % (self.message)
+ def __str__(self) -> str:
+ return '%s' % self.message
+
+ def __reduce__(self) -> Tuple[type, Tuple[str]]:
+ return self.__class__, (self.message,)
class Unauthorized(TelegramError):
- pass
+ """Raised when the bot has not enough rights to perform the requested action."""
+
+ __slots__ = ()
class InvalidToken(TelegramError):
+ """Raised when the token is invalid."""
+
+ __slots__ = ()
- def __init__(self):
- super(InvalidToken, self).__init__('Invalid token')
+ def __init__(self) -> None:
+ super().__init__('Invalid token')
+
+ def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override]
+ return self.__class__, ()
class NetworkError(TelegramError):
- pass
+ """Base class for exceptions due to networking errors."""
+
+ __slots__ = ()
class BadRequest(NetworkError):
- pass
+ """Raised when Telegram could not process the request correctly."""
+
+ __slots__ = ()
class TimedOut(NetworkError):
+ """Raised when a request took too long to finish."""
+
+ __slots__ = ()
+
+ def __init__(self) -> None:
+ super().__init__('Timed out')
- def __init__(self):
- super(TimedOut, self).__init__('Timed out')
+ def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override]
+ return self.__class__, ()
class ChatMigrated(TelegramError):
"""
+ Raised when the requested group chat migrated to supergroup and has a new chat id.
+
Args:
- new_chat_id (:obj:`int`):
+ new_chat_id (:obj:`int`): The new chat id of the group.
"""
- def __init__(self, new_chat_id):
- super(ChatMigrated,
- self).__init__('Group migrated to supergroup. New chat id: {}'.format(new_chat_id))
+ __slots__ = ('new_chat_id',)
+
+ def __init__(self, new_chat_id: int):
+ super().__init__(f'Group migrated to supergroup. New chat id: {new_chat_id}')
self.new_chat_id = new_chat_id
+ def __reduce__(self) -> Tuple[type, Tuple[int]]: # type: ignore[override]
+ return self.__class__, (self.new_chat_id,)
+
class RetryAfter(TelegramError):
"""
+ Raised when flood limits where exceeded.
+
Args:
- retry_after (:obj:`int`):
+ retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request.
"""
- def __init__(self, retry_after):
- super(RetryAfter,
- self).__init__('Flood control exceeded. Retry in {} seconds'.format(retry_after))
+ __slots__ = ('retry_after',)
+
+ def __init__(self, retry_after: int):
+ super().__init__(f'Flood control exceeded. Retry in {float(retry_after)} seconds')
self.retry_after = float(retry_after)
+
+ def __reduce__(self) -> Tuple[type, Tuple[float]]: # type: ignore[override]
+ return self.__class__, (self.retry_after,)
+
+
+class Conflict(TelegramError):
+ """Raised when a long poll or webhook conflicts with another one."""
+
+ __slots__ = ()
+
+ def __reduce__(self) -> Tuple[type, Tuple[str]]:
+ return self.__class__, (self.message,)
diff --git a/telegramer/include/telegram/ext/__init__.py b/telegramer/include/telegram/ext/__init__.py
index 1c69d0b..1d3a8fe 100644
--- a/telegramer/include/telegram/ext/__init__.py
+++ b/telegramer/include/telegram/ext/__init__.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,18 +16,37 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=C0413
"""Extensions over the Telegram Bot API to facilitate bot making"""
+from .extbot import ExtBot
+from .basepersistence import BasePersistence
+from .picklepersistence import PicklePersistence
+from .dictpersistence import DictPersistence
+from .handler import Handler
+from .callbackcontext import CallbackContext
+from .contexttypes import ContextTypes
from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async
+
+# https://bugs.python.org/issue41451, fixed on 3.7+, doesn't actually remove slots
+# try-except is just here in case the __init__ is called twice (like in the tests)
+# this block is also the reason for the pylint-ignore at the top of the file
+try:
+ del Dispatcher.__slots__
+except AttributeError as exc:
+ if str(exc) == '__slots__':
+ pass
+ else:
+ raise exc
+
from .jobqueue import JobQueue, Job
from .updater import Updater
from .callbackqueryhandler import CallbackQueryHandler
from .choseninlineresulthandler import ChosenInlineResultHandler
-from .commandhandler import CommandHandler
-from .handler import Handler
from .inlinequeryhandler import InlineQueryHandler
+from .filters import BaseFilter, MessageFilter, UpdateFilter, Filters
from .messagehandler import MessageHandler
-from .filters import BaseFilter, Filters
+from .commandhandler import CommandHandler, PrefixHandler
from .regexhandler import RegexHandler
from .stringcommandhandler import StringCommandHandler
from .stringregexhandler import StringRegexHandler
@@ -37,10 +56,51 @@
from .shippingqueryhandler import ShippingQueryHandler
from .messagequeue import MessageQueue
from .messagequeue import DelayQueue
+from .pollanswerhandler import PollAnswerHandler
+from .pollhandler import PollHandler
+from .chatmemberhandler import ChatMemberHandler
+from .chatjoinrequesthandler import ChatJoinRequestHandler
+from .defaults import Defaults
+from .callbackdatacache import CallbackDataCache, InvalidCallbackData
-__all__ = ('Dispatcher', 'JobQueue', 'Job', 'Updater', 'CallbackQueryHandler',
- 'ChosenInlineResultHandler', 'CommandHandler', 'Handler', 'InlineQueryHandler',
- 'MessageHandler', 'BaseFilter', 'Filters', 'RegexHandler', 'StringCommandHandler',
- 'StringRegexHandler', 'TypeHandler', 'ConversationHandler',
- 'PreCheckoutQueryHandler', 'ShippingQueryHandler', 'MessageQueue', 'DelayQueue',
- 'DispatcherHandlerStop', 'run_async')
+__all__ = (
+ 'BaseFilter',
+ 'BasePersistence',
+ 'CallbackContext',
+ 'CallbackDataCache',
+ 'CallbackQueryHandler',
+ 'ChatJoinRequestHandler',
+ 'ChatMemberHandler',
+ 'ChosenInlineResultHandler',
+ 'CommandHandler',
+ 'ContextTypes',
+ 'ConversationHandler',
+ 'Defaults',
+ 'DelayQueue',
+ 'DictPersistence',
+ 'Dispatcher',
+ 'DispatcherHandlerStop',
+ 'ExtBot',
+ 'Filters',
+ 'Handler',
+ 'InlineQueryHandler',
+ 'InvalidCallbackData',
+ 'Job',
+ 'JobQueue',
+ 'MessageFilter',
+ 'MessageHandler',
+ 'MessageQueue',
+ 'PicklePersistence',
+ 'PollAnswerHandler',
+ 'PollHandler',
+ 'PreCheckoutQueryHandler',
+ 'PrefixHandler',
+ 'RegexHandler',
+ 'ShippingQueryHandler',
+ 'StringCommandHandler',
+ 'StringRegexHandler',
+ 'TypeHandler',
+ 'UpdateFilter',
+ 'Updater',
+ 'run_async',
+)
diff --git a/telegramer/include/telegram/ext/basepersistence.py b/telegramer/include/telegram/ext/basepersistence.py
new file mode 100644
index 0000000..20afa1d
--- /dev/null
+++ b/telegramer/include/telegram/ext/basepersistence.py
@@ -0,0 +1,566 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains the BasePersistence class."""
+import warnings
+from sys import version_info as py_ver
+from abc import ABC, abstractmethod
+from copy import copy
+from typing import Dict, Optional, Tuple, cast, ClassVar, Generic, DefaultDict
+
+from telegram.utils.deprecate import set_new_attribute_deprecated
+
+from telegram import Bot
+import telegram.ext.extbot
+
+from telegram.ext.utils.types import UD, CD, BD, ConversationDict, CDCData
+
+
+class BasePersistence(Generic[UD, CD, BD], ABC):
+ """Interface class for adding persistence to your bot.
+ Subclass this object for different implementations of a persistent bot.
+
+ All relevant methods must be overwritten. This includes:
+
+ * :meth:`get_bot_data`
+ * :meth:`update_bot_data`
+ * :meth:`refresh_bot_data`
+ * :meth:`get_chat_data`
+ * :meth:`update_chat_data`
+ * :meth:`refresh_chat_data`
+ * :meth:`get_user_data`
+ * :meth:`update_user_data`
+ * :meth:`refresh_user_data`
+ * :meth:`get_callback_data`
+ * :meth:`update_callback_data`
+ * :meth:`get_conversations`
+ * :meth:`update_conversation`
+ * :meth:`flush`
+
+ If you don't actually need one of those methods, a simple ``pass`` is enough. For example, if
+ ``store_bot_data=False``, you don't need :meth:`get_bot_data`, :meth:`update_bot_data` or
+ :meth:`refresh_bot_data`.
+
+ Warning:
+ Persistence will try to replace :class:`telegram.Bot` instances by :attr:`REPLACED_BOT` and
+ insert the bot set with :meth:`set_bot` upon loading of the data. This is to ensure that
+ changes to the bot apply to the saved objects, too. If you change the bots token, this may
+ lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see
+ :meth:`replace_bot` and :meth:`insert_bot`.
+
+ Note:
+ :meth:`replace_bot` and :meth:`insert_bot` are used *independently* of the implementation
+ of the :meth:`update/get_*` methods, i.e. you don't need to worry about it while
+ implementing a custom persistence subclass.
+
+ Args:
+ store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
+ persistence class. Default is :obj:`True`.
+ store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this
+ persistence class. Default is :obj:`True` .
+ store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this
+ persistence class. Default is :obj:`True`.
+ store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this
+ persistence class. Default is :obj:`False`.
+
+ .. versionadded:: 13.6
+
+ Attributes:
+ store_user_data (:obj:`bool`): Optional, Whether user_data should be saved by this
+ persistence class.
+ store_chat_data (:obj:`bool`): Optional. Whether chat_data should be saved by this
+ persistence class.
+ store_bot_data (:obj:`bool`): Optional. Whether bot_data should be saved by this
+ persistence class.
+ store_callback_data (:obj:`bool`): Optional. Whether callback_data should be saved by this
+ persistence class.
+
+ .. versionadded:: 13.6
+ """
+
+ # Apparently Py 3.7 and below have '__dict__' in ABC
+ if py_ver < (3, 7):
+ __slots__ = (
+ 'store_user_data',
+ 'store_chat_data',
+ 'store_bot_data',
+ 'store_callback_data',
+ 'bot',
+ )
+ else:
+ __slots__ = (
+ 'store_user_data', # type: ignore[assignment]
+ 'store_chat_data',
+ 'store_bot_data',
+ 'store_callback_data',
+ 'bot',
+ '__dict__',
+ )
+
+ def __new__(
+ cls, *args: object, **kwargs: object # pylint: disable=W0613
+ ) -> 'BasePersistence':
+ """This overrides the get_* and update_* methods to use insert/replace_bot.
+ That has the side effect that we always pass deepcopied data to those methods, so in
+ Pickle/DictPersistence we don't have to worry about copying the data again.
+
+ Note: This doesn't hold for second tuple-entry of callback_data. That's a Dict[str, str],
+ so no bots to replace anyway.
+ """
+ instance = super().__new__(cls)
+ get_user_data = instance.get_user_data
+ get_chat_data = instance.get_chat_data
+ get_bot_data = instance.get_bot_data
+ get_callback_data = instance.get_callback_data
+ update_user_data = instance.update_user_data
+ update_chat_data = instance.update_chat_data
+ update_bot_data = instance.update_bot_data
+ update_callback_data = instance.update_callback_data
+
+ def get_user_data_insert_bot() -> DefaultDict[int, UD]:
+ return instance.insert_bot(get_user_data())
+
+ def get_chat_data_insert_bot() -> DefaultDict[int, CD]:
+ return instance.insert_bot(get_chat_data())
+
+ def get_bot_data_insert_bot() -> BD:
+ return instance.insert_bot(get_bot_data())
+
+ def get_callback_data_insert_bot() -> Optional[CDCData]:
+ cdc_data = get_callback_data()
+ if cdc_data is None:
+ return None
+ return instance.insert_bot(cdc_data[0]), cdc_data[1]
+
+ def update_user_data_replace_bot(user_id: int, data: UD) -> None:
+ return update_user_data(user_id, instance.replace_bot(data))
+
+ def update_chat_data_replace_bot(chat_id: int, data: CD) -> None:
+ return update_chat_data(chat_id, instance.replace_bot(data))
+
+ def update_bot_data_replace_bot(data: BD) -> None:
+ return update_bot_data(instance.replace_bot(data))
+
+ def update_callback_data_replace_bot(data: CDCData) -> None:
+ obj_data, queue = data
+ return update_callback_data((instance.replace_bot(obj_data), queue))
+
+ # We want to ignore TGDeprecation warnings so we use obj.__setattr__. Adds to __dict__
+ object.__setattr__(instance, 'get_user_data', get_user_data_insert_bot)
+ object.__setattr__(instance, 'get_chat_data', get_chat_data_insert_bot)
+ object.__setattr__(instance, 'get_bot_data', get_bot_data_insert_bot)
+ object.__setattr__(instance, 'get_callback_data', get_callback_data_insert_bot)
+ object.__setattr__(instance, 'update_user_data', update_user_data_replace_bot)
+ object.__setattr__(instance, 'update_chat_data', update_chat_data_replace_bot)
+ object.__setattr__(instance, 'update_bot_data', update_bot_data_replace_bot)
+ object.__setattr__(instance, 'update_callback_data', update_callback_data_replace_bot)
+ return instance
+
+ def __init__(
+ self,
+ store_user_data: bool = True,
+ store_chat_data: bool = True,
+ store_bot_data: bool = True,
+ store_callback_data: bool = False,
+ ):
+ self.store_user_data = store_user_data
+ self.store_chat_data = store_chat_data
+ self.store_bot_data = store_bot_data
+ self.store_callback_data = store_callback_data
+ self.bot: Bot = None # type: ignore[assignment]
+
+ def __setattr__(self, key: str, value: object) -> None:
+ # Allow user defined subclasses to have custom attributes.
+ if issubclass(self.__class__, BasePersistence) and self.__class__.__name__ not in {
+ 'DictPersistence',
+ 'PicklePersistence',
+ }:
+ object.__setattr__(self, key, value)
+ return
+ set_new_attribute_deprecated(self, key, value)
+
+ def set_bot(self, bot: Bot) -> None:
+ """Set the Bot to be used by this persistence instance.
+
+ Args:
+ bot (:class:`telegram.Bot`): The bot.
+ """
+ if self.store_callback_data and not isinstance(bot, telegram.ext.extbot.ExtBot):
+ raise TypeError('store_callback_data can only be used with telegram.ext.ExtBot.')
+
+ self.bot = bot
+
+ @classmethod
+ def replace_bot(cls, obj: object) -> object:
+ """
+ Replaces all instances of :class:`telegram.Bot` that occur within the passed object with
+ :attr:`REPLACED_BOT`. Currently, this handles objects of type ``list``, ``tuple``, ``set``,
+ ``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or
+ ``__slots__`` attribute, excluding classes and objects that can't be copied with
+ ``copy.copy``. If the parsing of an object fails, the object will be returned unchanged and
+ the error will be logged.
+
+ Args:
+ obj (:obj:`object`): The object
+
+ Returns:
+ :obj:`obj`: Copy of the object with Bot instances replaced.
+ """
+ return cls._replace_bot(obj, {})
+
+ @classmethod
+ def _replace_bot(cls, obj: object, memo: Dict[int, object]) -> object: # pylint: disable=R0911
+ obj_id = id(obj)
+ if obj_id in memo:
+ return memo[obj_id]
+
+ if isinstance(obj, Bot):
+ memo[obj_id] = cls.REPLACED_BOT
+ return cls.REPLACED_BOT
+ if isinstance(obj, (list, set)):
+ # We copy the iterable here for thread safety, i.e. make sure the object we iterate
+ # over doesn't change its length during the iteration
+ temp_iterable = obj.copy()
+ new_iterable = obj.__class__(cls._replace_bot(item, memo) for item in temp_iterable)
+ memo[obj_id] = new_iterable
+ return new_iterable
+ if isinstance(obj, (tuple, frozenset)):
+ # tuples and frozensets are immutable so we don't need to worry about thread safety
+ new_immutable = obj.__class__(cls._replace_bot(item, memo) for item in obj)
+ memo[obj_id] = new_immutable
+ return new_immutable
+ if isinstance(obj, type):
+ # classes usually do have a __dict__, but it's not writable
+ warnings.warn(
+ 'BasePersistence.replace_bot does not handle classes. See '
+ 'the docs of BasePersistence.replace_bot for more information.',
+ RuntimeWarning,
+ )
+ return obj
+
+ try:
+ new_obj = copy(obj)
+ memo[obj_id] = new_obj
+ except Exception:
+ warnings.warn(
+ 'BasePersistence.replace_bot does not handle objects that can not be copied. See '
+ 'the docs of BasePersistence.replace_bot for more information.',
+ RuntimeWarning,
+ )
+ memo[obj_id] = obj
+ return obj
+
+ if isinstance(obj, dict):
+ # We handle dicts via copy(obj) so we don't have to make a
+ # difference between dict and defaultdict
+ new_obj = cast(dict, new_obj)
+ # We can't iterate over obj.items() due to thread safety, i.e. the dicts length may
+ # change during the iteration
+ temp_dict = new_obj.copy()
+ new_obj.clear()
+ for k, val in temp_dict.items():
+ new_obj[cls._replace_bot(k, memo)] = cls._replace_bot(val, memo)
+ memo[obj_id] = new_obj
+ return new_obj
+ try:
+ if hasattr(obj, '__slots__'):
+ for attr_name in new_obj.__slots__:
+ setattr(
+ new_obj,
+ attr_name,
+ cls._replace_bot(
+ cls._replace_bot(getattr(new_obj, attr_name), memo), memo
+ ),
+ )
+ if '__dict__' in obj.__slots__:
+ # In this case, we have already covered the case that obj has __dict__
+ # Note that obj may have a __dict__ even if it's not in __slots__!
+ memo[obj_id] = new_obj
+ return new_obj
+ if hasattr(obj, '__dict__'):
+ for attr_name, attr in new_obj.__dict__.items():
+ setattr(new_obj, attr_name, cls._replace_bot(attr, memo))
+ memo[obj_id] = new_obj
+ return new_obj
+ except Exception as exception:
+ warnings.warn(
+ f'Parsing of an object failed with the following exception: {exception}. '
+ f'See the docs of BasePersistence.replace_bot for more information.',
+ RuntimeWarning,
+ )
+
+ memo[obj_id] = obj
+ return obj
+
+ def insert_bot(self, obj: object) -> object:
+ """
+ Replaces all instances of :attr:`REPLACED_BOT` that occur within the passed object with
+ :attr:`bot`. Currently, this handles objects of type ``list``, ``tuple``, ``set``,
+ ``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or
+ ``__slots__`` attribute, excluding classes and objects that can't be copied with
+ ``copy.copy``. If the parsing of an object fails, the object will be returned unchanged and
+ the error will be logged.
+
+ Args:
+ obj (:obj:`object`): The object
+
+ Returns:
+ :obj:`obj`: Copy of the object with Bot instances inserted.
+ """
+ return self._insert_bot(obj, {})
+
+ def _insert_bot(self, obj: object, memo: Dict[int, object]) -> object: # pylint: disable=R0911
+ obj_id = id(obj)
+ if obj_id in memo:
+ return memo[obj_id]
+
+ if isinstance(obj, Bot):
+ memo[obj_id] = self.bot
+ return self.bot
+ if isinstance(obj, str) and obj == self.REPLACED_BOT:
+ memo[obj_id] = self.bot
+ return self.bot
+ if isinstance(obj, (list, set)):
+ # We copy the iterable here for thread safety, i.e. make sure the object we iterate
+ # over doesn't change its length during the iteration
+ temp_iterable = obj.copy()
+ new_iterable = obj.__class__(self._insert_bot(item, memo) for item in temp_iterable)
+ memo[obj_id] = new_iterable
+ return new_iterable
+ if isinstance(obj, (tuple, frozenset)):
+ # tuples and frozensets are immutable so we don't need to worry about thread safety
+ new_immutable = obj.__class__(self._insert_bot(item, memo) for item in obj)
+ memo[obj_id] = new_immutable
+ return new_immutable
+ if isinstance(obj, type):
+ # classes usually do have a __dict__, but it's not writable
+ warnings.warn(
+ 'BasePersistence.insert_bot does not handle classes. See '
+ 'the docs of BasePersistence.insert_bot for more information.',
+ RuntimeWarning,
+ )
+ return obj
+
+ try:
+ new_obj = copy(obj)
+ except Exception:
+ warnings.warn(
+ 'BasePersistence.insert_bot does not handle objects that can not be copied. See '
+ 'the docs of BasePersistence.insert_bot for more information.',
+ RuntimeWarning,
+ )
+ memo[obj_id] = obj
+ return obj
+
+ if isinstance(obj, dict):
+ # We handle dicts via copy(obj) so we don't have to make a
+ # difference between dict and defaultdict
+ new_obj = cast(dict, new_obj)
+ # We can't iterate over obj.items() due to thread safety, i.e. the dicts length may
+ # change during the iteration
+ temp_dict = new_obj.copy()
+ new_obj.clear()
+ for k, val in temp_dict.items():
+ new_obj[self._insert_bot(k, memo)] = self._insert_bot(val, memo)
+ memo[obj_id] = new_obj
+ return new_obj
+ try:
+ if hasattr(obj, '__slots__'):
+ for attr_name in obj.__slots__:
+ setattr(
+ new_obj,
+ attr_name,
+ self._insert_bot(
+ self._insert_bot(getattr(new_obj, attr_name), memo), memo
+ ),
+ )
+ if '__dict__' in obj.__slots__:
+ # In this case, we have already covered the case that obj has __dict__
+ # Note that obj may have a __dict__ even if it's not in __slots__!
+ memo[obj_id] = new_obj
+ return new_obj
+ if hasattr(obj, '__dict__'):
+ for attr_name, attr in new_obj.__dict__.items():
+ setattr(new_obj, attr_name, self._insert_bot(attr, memo))
+ memo[obj_id] = new_obj
+ return new_obj
+ except Exception as exception:
+ warnings.warn(
+ f'Parsing of an object failed with the following exception: {exception}. '
+ f'See the docs of BasePersistence.insert_bot for more information.',
+ RuntimeWarning,
+ )
+
+ memo[obj_id] = obj
+ return obj
+
+ @abstractmethod
+ def get_user_data(self) -> DefaultDict[int, UD]:
+ """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
+ persistence object. It should return the ``user_data`` if stored, or an empty
+ :obj:`defaultdict(telegram.ext.utils.types.UD)` with integer keys.
+
+ Returns:
+ DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.UD`]: The restored user data.
+ """
+
+ @abstractmethod
+ def get_chat_data(self) -> DefaultDict[int, CD]:
+ """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
+ persistence object. It should return the ``chat_data`` if stored, or an empty
+ :obj:`defaultdict(telegram.ext.utils.types.CD)` with integer keys.
+
+ Returns:
+ DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.CD`]: The restored chat data.
+ """
+
+ @abstractmethod
+ def get_bot_data(self) -> BD:
+ """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
+ persistence object. It should return the ``bot_data`` if stored, or an empty
+ :class:`telegram.ext.utils.types.BD`.
+
+ Returns:
+ :class:`telegram.ext.utils.types.BD`: The restored bot data.
+ """
+
+ def get_callback_data(self) -> Optional[CDCData]:
+ """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
+ persistence object. If callback data was stored, it should be returned.
+
+ .. versionadded:: 13.6
+
+ Returns:
+ Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or
+ :obj:`None`, if no data was stored.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def get_conversations(self, name: str) -> ConversationDict:
+ """Will be called by :class:`telegram.ext.Dispatcher` when a
+ :class:`telegram.ext.ConversationHandler` is added if
+ :attr:`telegram.ext.ConversationHandler.persistent` is :obj:`True`.
+ It should return the conversations for the handler with `name` or an empty :obj:`dict`
+
+ Args:
+ name (:obj:`str`): The handlers name.
+
+ Returns:
+ :obj:`dict`: The restored conversations for the handler.
+ """
+
+ @abstractmethod
+ def update_conversation(
+ self, name: str, key: Tuple[int, ...], new_state: Optional[object]
+ ) -> None:
+ """Will be called when a :class:`telegram.ext.ConversationHandler` changes states.
+ This allows the storage of the new state in the persistence.
+
+ Args:
+ name (:obj:`str`): The handler's name.
+ key (:obj:`tuple`): The key the state is changed for.
+ new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
+ """
+
+ @abstractmethod
+ def update_user_data(self, user_id: int, data: UD) -> None:
+ """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
+ handled an update.
+
+ Args:
+ user_id (:obj:`int`): The user the data might have been changed for.
+ data (:class:`telegram.ext.utils.types.UD`): The
+ :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``.
+ """
+
+ @abstractmethod
+ def update_chat_data(self, chat_id: int, data: CD) -> None:
+ """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
+ handled an update.
+
+ Args:
+ chat_id (:obj:`int`): The chat the data might have been changed for.
+ data (:class:`telegram.ext.utils.types.CD`): The
+ :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``.
+ """
+
+ @abstractmethod
+ def update_bot_data(self, data: BD) -> None:
+ """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
+ handled an update.
+
+ Args:
+ data (:class:`telegram.ext.utils.types.BD`): The
+ :attr:`telegram.ext.Dispatcher.bot_data`.
+ """
+
+ def refresh_user_data(self, user_id: int, user_data: UD) -> None:
+ """Will be called by the :class:`telegram.ext.Dispatcher` before passing the
+ :attr:`user_data` to a callback. Can be used to update data stored in :attr:`user_data`
+ from an external source.
+
+ .. versionadded:: 13.6
+
+ Args:
+ user_id (:obj:`int`): The user ID this :attr:`user_data` is associated with.
+ user_data (:class:`telegram.ext.utils.types.UD`): The ``user_data`` of a single user.
+ """
+
+ def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None:
+ """Will be called by the :class:`telegram.ext.Dispatcher` before passing the
+ :attr:`chat_data` to a callback. Can be used to update data stored in :attr:`chat_data`
+ from an external source.
+
+ .. versionadded:: 13.6
+
+ Args:
+ chat_id (:obj:`int`): The chat ID this :attr:`chat_data` is associated with.
+ chat_data (:class:`telegram.ext.utils.types.CD`): The ``chat_data`` of a single chat.
+ """
+
+ def refresh_bot_data(self, bot_data: BD) -> None:
+ """Will be called by the :class:`telegram.ext.Dispatcher` before passing the
+ :attr:`bot_data` to a callback. Can be used to update data stored in :attr:`bot_data`
+ from an external source.
+
+ .. versionadded:: 13.6
+
+ Args:
+ bot_data (:class:`telegram.ext.utils.types.BD`): The ``bot_data``.
+ """
+
+ def update_callback_data(self, data: CDCData) -> None:
+ """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
+ handled an update.
+
+ .. versionadded:: 13.6
+
+ Args:
+ data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore
+ :class:`telegram.ext.CallbackDataCache`.
+ """
+ raise NotImplementedError
+
+ def flush(self) -> None:
+ """Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the
+ persistence a chance to finish up saving or close a database connection gracefully.
+ """
+
+ REPLACED_BOT: ClassVar[str] = 'bot_instance_replaced_by_ptb_persistence'
+ """:obj:`str`: Placeholder for :class:`telegram.Bot` instances replaced in saved data."""
diff --git a/telegramer/include/telegram/ext/callbackcontext.py b/telegramer/include/telegram/ext/callbackcontext.py
new file mode 100644
index 0000000..607ca92
--- /dev/null
+++ b/telegramer/include/telegram/ext/callbackcontext.py
@@ -0,0 +1,361 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=R0201
+"""This module contains the CallbackContext class."""
+from queue import Queue
+from typing import (
+ TYPE_CHECKING,
+ Dict,
+ List,
+ Match,
+ NoReturn,
+ Optional,
+ Tuple,
+ Union,
+ Generic,
+ Type,
+ TypeVar,
+)
+
+from telegram import Update, CallbackQuery
+from telegram.ext import ExtBot
+from telegram.ext.utils.types import UD, CD, BD
+
+if TYPE_CHECKING:
+ from telegram import Bot
+ from telegram.ext import Dispatcher, Job, JobQueue
+
+CC = TypeVar('CC', bound='CallbackContext')
+
+
+class CallbackContext(Generic[UD, CD, BD]):
+ """
+ This is a context object passed to the callback called by :class:`telegram.ext.Handler`
+ or by the :class:`telegram.ext.Dispatcher` in an error handler added by
+ :attr:`telegram.ext.Dispatcher.add_error_handler` or to the callback of a
+ :class:`telegram.ext.Job`.
+
+ Note:
+ :class:`telegram.ext.Dispatcher` will create a single context for an entire update. This
+ means that if you got 2 handlers in different groups and they both get called, they will
+ get passed the same `CallbackContext` object (of course with proper attributes like
+ `.matches` differing). This allows you to add custom attributes in a lower handler group
+ callback, and then subsequently access those attributes in a higher handler group callback.
+ Note that the attributes on `CallbackContext` might change in the future, so make sure to
+ use a fairly unique name for the attributes.
+
+ Warning:
+ Do not combine custom attributes and ``@run_async``/
+ :meth:`telegram.ext.Disptacher.run_async`. Due to how ``run_async`` works, it will
+ almost certainly execute the callbacks for an update out of order, and the attributes
+ that you think you added will not be present.
+
+ Args:
+ dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context.
+
+ Attributes:
+ matches (List[:obj:`re match object`]): Optional. If the associated update originated from
+ a regex-supported handler or had a :class:`Filters.regex`, this will contain a list of
+ match objects for every pattern where ``re.search(pattern, string)`` returned a match.
+ Note that filters short circuit, so combined regex filters will not always
+ be evaluated.
+ args (List[:obj:`str`]): Optional. Arguments passed to a command if the associated update
+ is handled by :class:`telegram.ext.CommandHandler`, :class:`telegram.ext.PrefixHandler`
+ or :class:`telegram.ext.StringCommandHandler`. It contains a list of the words in the
+ text after the command, using any whitespace string as a delimiter.
+ error (:obj:`Exception`): Optional. The error that was raised. Only present when passed
+ to a error handler registered with :attr:`telegram.ext.Dispatcher.add_error_handler`.
+ async_args (List[:obj:`object`]): Optional. Positional arguments of the function that
+ raised the error. Only present when the raising function was run asynchronously using
+ :meth:`telegram.ext.Dispatcher.run_async`.
+ async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the function
+ that raised the error. Only present when the raising function was run asynchronously
+ using :meth:`telegram.ext.Dispatcher.run_async`.
+ job (:class:`telegram.ext.Job`): Optional. The job which originated this callback.
+ Only present when passed to the callback of :class:`telegram.ext.Job`.
+
+ """
+
+ __slots__ = (
+ '_dispatcher',
+ '_chat_id_and_data',
+ '_user_id_and_data',
+ 'args',
+ 'matches',
+ 'error',
+ 'job',
+ 'async_args',
+ 'async_kwargs',
+ '__dict__',
+ )
+
+ def __init__(self, dispatcher: 'Dispatcher'):
+ """
+ Args:
+ dispatcher (:class:`telegram.ext.Dispatcher`):
+ """
+ if not dispatcher.use_context:
+ raise ValueError(
+ 'CallbackContext should not be used with a non context aware ' 'dispatcher!'
+ )
+ self._dispatcher = dispatcher
+ self._chat_id_and_data: Optional[Tuple[int, CD]] = None
+ self._user_id_and_data: Optional[Tuple[int, UD]] = None
+ self.args: Optional[List[str]] = None
+ self.matches: Optional[List[Match]] = None
+ self.error: Optional[Exception] = None
+ self.job: Optional['Job'] = None
+ self.async_args: Optional[Union[List, Tuple]] = None
+ self.async_kwargs: Optional[Dict[str, object]] = None
+
+ @property
+ def dispatcher(self) -> 'Dispatcher':
+ """:class:`telegram.ext.Dispatcher`: The dispatcher associated with this context."""
+ return self._dispatcher
+
+ @property
+ def bot_data(self) -> BD:
+ """:obj:`dict`: Optional. A dict that can be used to keep any data in. For each
+ update it will be the same ``dict``.
+ """
+ return self.dispatcher.bot_data
+
+ @bot_data.setter
+ def bot_data(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to bot_data, see https://git.io/Jt6ic"
+ )
+
+ @property
+ def chat_data(self) -> Optional[CD]:
+ """:obj:`dict`: Optional. A dict that can be used to keep any data in. For each
+ update from the same chat id it will be the same ``dict``.
+
+ Warning:
+ When a group chat migrates to a supergroup, its chat id will change and the
+ ``chat_data`` needs to be transferred. For details see our `wiki page
+ `_.
+ """
+ if self._chat_id_and_data:
+ return self._chat_id_and_data[1]
+ return None
+
+ @chat_data.setter
+ def chat_data(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to chat_data, see https://git.io/Jt6ic"
+ )
+
+ @property
+ def user_data(self) -> Optional[UD]:
+ """:obj:`dict`: Optional. A dict that can be used to keep any data in. For each
+ update from the same user it will be the same ``dict``.
+ """
+ if self._user_id_and_data:
+ return self._user_id_and_data[1]
+ return None
+
+ @user_data.setter
+ def user_data(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to user_data, see https://git.io/Jt6ic"
+ )
+
+ def refresh_data(self) -> None:
+ """If :attr:`dispatcher` uses persistence, calls
+ :meth:`telegram.ext.BasePersistence.refresh_bot_data` on :attr:`bot_data`,
+ :meth:`telegram.ext.BasePersistence.refresh_chat_data` on :attr:`chat_data` and
+ :meth:`telegram.ext.BasePersistence.refresh_user_data` on :attr:`user_data`, if
+ appropriate.
+
+ .. versionadded:: 13.6
+ """
+ if self.dispatcher.persistence:
+ if self.dispatcher.persistence.store_bot_data:
+ self.dispatcher.persistence.refresh_bot_data(self.bot_data)
+ if self.dispatcher.persistence.store_chat_data and self._chat_id_and_data is not None:
+ self.dispatcher.persistence.refresh_chat_data(*self._chat_id_and_data)
+ if self.dispatcher.persistence.store_user_data and self._user_id_and_data is not None:
+ self.dispatcher.persistence.refresh_user_data(*self._user_id_and_data)
+
+ def drop_callback_data(self, callback_query: CallbackQuery) -> None:
+ """
+ Deletes the cached data for the specified callback query.
+
+ .. versionadded:: 13.6
+
+ Note:
+ Will *not* raise exceptions in case the data is not found in the cache.
+ *Will* raise :class:`KeyError` in case the callback query can not be found in the
+ cache.
+
+ Args:
+ callback_query (:class:`telegram.CallbackQuery`): The callback query.
+
+ Raises:
+ KeyError | RuntimeError: :class:`KeyError`, if the callback query can not be found in
+ the cache and :class:`RuntimeError`, if the bot doesn't allow for arbitrary
+ callback data.
+ """
+ if isinstance(self.bot, ExtBot):
+ if not self.bot.arbitrary_callback_data:
+ raise RuntimeError(
+ 'This telegram.ext.ExtBot instance does not use arbitrary callback data.'
+ )
+ self.bot.callback_data_cache.drop_data(callback_query)
+ else:
+ raise RuntimeError('telegram.Bot does not allow for arbitrary callback data.')
+
+ @classmethod
+ def from_error(
+ cls: Type[CC],
+ update: object,
+ error: Exception,
+ dispatcher: 'Dispatcher',
+ async_args: Union[List, Tuple] = None,
+ async_kwargs: Dict[str, object] = None,
+ ) -> CC:
+ """
+ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error
+ handlers.
+
+ .. seealso:: :meth:`telegram.ext.Dispatcher.add_error_handler`
+
+ Args:
+ update (:obj:`object` | :class:`telegram.Update`): The update associated with the
+ error. May be :obj:`None`, e.g. for errors in job callbacks.
+ error (:obj:`Exception`): The error.
+ dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
+ context.
+ async_args (List[:obj:`object`]): Optional. Positional arguments of the function that
+ raised the error. Pass only when the raising function was run asynchronously using
+ :meth:`telegram.ext.Dispatcher.run_async`.
+ async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the
+ function that raised the error. Pass only when the raising function was run
+ asynchronously using :meth:`telegram.ext.Dispatcher.run_async`.
+
+ Returns:
+ :class:`telegram.ext.CallbackContext`
+ """
+ self = cls.from_update(update, dispatcher)
+ self.error = error
+ self.async_args = async_args
+ self.async_kwargs = async_kwargs
+ return self
+
+ @classmethod
+ def from_update(cls: Type[CC], update: object, dispatcher: 'Dispatcher') -> CC:
+ """
+ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the
+ handlers.
+
+ .. seealso:: :meth:`telegram.ext.Dispatcher.add_handler`
+
+ Args:
+ update (:obj:`object` | :class:`telegram.Update`): The update.
+ dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
+ context.
+
+ Returns:
+ :class:`telegram.ext.CallbackContext`
+ """
+ self = cls(dispatcher)
+
+ if update is not None and isinstance(update, Update):
+ chat = update.effective_chat
+ user = update.effective_user
+
+ if chat:
+ self._chat_id_and_data = (
+ chat.id,
+ dispatcher.chat_data[chat.id], # pylint: disable=W0212
+ )
+ if user:
+ self._user_id_and_data = (
+ user.id,
+ dispatcher.user_data[user.id], # pylint: disable=W0212
+ )
+ return self
+
+ @classmethod
+ def from_job(cls: Type[CC], job: 'Job', dispatcher: 'Dispatcher') -> CC:
+ """
+ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a
+ job callback.
+
+ .. seealso:: :meth:`telegram.ext.JobQueue`
+
+ Args:
+ job (:class:`telegram.ext.Job`): The job.
+ dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
+ context.
+
+ Returns:
+ :class:`telegram.ext.CallbackContext`
+ """
+ self = cls(dispatcher)
+ self.job = job
+ return self
+
+ def update(self, data: Dict[str, object]) -> None:
+ """Updates ``self.__slots__`` with the passed data.
+
+ Args:
+ data (Dict[:obj:`str`, :obj:`object`]): The data.
+ """
+ for key, value in data.items():
+ setattr(self, key, value)
+
+ @property
+ def bot(self) -> 'Bot':
+ """:class:`telegram.Bot`: The bot associated with this context."""
+ return self._dispatcher.bot
+
+ @property
+ def job_queue(self) -> Optional['JobQueue']:
+ """
+ :class:`telegram.ext.JobQueue`: The ``JobQueue`` used by the
+ :class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater`
+ associated with this context.
+
+ """
+ return self._dispatcher.job_queue
+
+ @property
+ def update_queue(self) -> Queue:
+ """
+ :class:`queue.Queue`: The ``Queue`` instance used by the
+ :class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater`
+ associated with this context.
+
+ """
+ return self._dispatcher.update_queue
+
+ @property
+ def match(self) -> Optional[Match[str]]:
+ """
+ `Regex match type`: The first match from :attr:`matches`.
+ Useful if you are only filtering using a single regex filter.
+ Returns `None` if :attr:`matches` is empty.
+ """
+ try:
+ return self.matches[0] # type: ignore[index] # pylint: disable=unsubscriptable-object
+ except (IndexError, TypeError):
+ return None
diff --git a/telegramer/include/telegram/ext/callbackdatacache.py b/telegramer/include/telegram/ext/callbackdatacache.py
new file mode 100644
index 0000000..df64005
--- /dev/null
+++ b/telegramer/include/telegram/ext/callbackdatacache.py
@@ -0,0 +1,408 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains the CallbackDataCache class."""
+import logging
+import time
+from datetime import datetime
+from threading import Lock
+from typing import Dict, Tuple, Union, Optional, MutableMapping, TYPE_CHECKING, cast
+from uuid import uuid4
+
+from cachetools import LRUCache # pylint: disable=E0401
+
+from telegram import (
+ InlineKeyboardMarkup,
+ InlineKeyboardButton,
+ TelegramError,
+ CallbackQuery,
+ Message,
+ User,
+)
+from telegram.utils.helpers import to_float_timestamp
+from telegram.ext.utils.types import CDCData
+
+if TYPE_CHECKING:
+ from telegram.ext import ExtBot
+
+
+class InvalidCallbackData(TelegramError):
+ """
+ Raised when the received callback data has been tempered with or deleted from cache.
+
+ .. versionadded:: 13.6
+
+ Args:
+ callback_data (:obj:`int`, optional): The button data of which the callback data could not
+ be found.
+
+ Attributes:
+ callback_data (:obj:`int`): Optional. The button data of which the callback data could not
+ be found.
+ """
+
+ __slots__ = ('callback_data',)
+
+ def __init__(self, callback_data: str = None) -> None:
+ super().__init__(
+ 'The object belonging to this callback_data was deleted or the callback_data was '
+ 'manipulated.'
+ )
+ self.callback_data = callback_data
+
+ def __reduce__(self) -> Tuple[type, Tuple[Optional[str]]]: # type: ignore[override]
+ return self.__class__, (self.callback_data,)
+
+
+class _KeyboardData:
+ __slots__ = ('keyboard_uuid', 'button_data', 'access_time')
+
+ def __init__(
+ self, keyboard_uuid: str, access_time: float = None, button_data: Dict[str, object] = None
+ ):
+ self.keyboard_uuid = keyboard_uuid
+ self.button_data = button_data or {}
+ self.access_time = access_time or time.time()
+
+ def update_access_time(self) -> None:
+ """Updates the access time with the current time."""
+ self.access_time = time.time()
+
+ def to_tuple(self) -> Tuple[str, float, Dict[str, object]]:
+ """Gives a tuple representation consisting of the keyboard uuid, the access time and the
+ button data.
+ """
+ return self.keyboard_uuid, self.access_time, self.button_data
+
+
+class CallbackDataCache:
+ """A custom cache for storing the callback data of a :class:`telegram.ext.ExtBot`. Internally,
+ it keeps two mappings with fixed maximum size:
+
+ * One for mapping the data received in callback queries to the cached objects
+ * One for mapping the IDs of received callback queries to the cached objects
+
+ The second mapping allows to manually drop data that has been cached for keyboards of messages
+ sent via inline mode.
+ If necessary, will drop the least recently used items.
+
+ .. versionadded:: 13.6
+
+ Args:
+ bot (:class:`telegram.ext.ExtBot`): The bot this cache is for.
+ maxsize (:obj:`int`, optional): Maximum number of items in each of the internal mappings.
+ Defaults to 1024.
+ persistent_data (:obj:`telegram.ext.utils.types.CDCData`, optional): Data to initialize
+ the cache with, as returned by :meth:`telegram.ext.BasePersistence.get_callback_data`.
+
+ Attributes:
+ bot (:class:`telegram.ext.ExtBot`): The bot this cache is for.
+ maxsize (:obj:`int`): maximum size of the cache.
+
+ """
+
+ __slots__ = ('bot', 'maxsize', '_keyboard_data', '_callback_queries', '__lock', 'logger')
+
+ def __init__(
+ self,
+ bot: 'ExtBot',
+ maxsize: int = 1024,
+ persistent_data: CDCData = None,
+ ):
+ self.logger = logging.getLogger(__name__)
+
+ self.bot = bot
+ self.maxsize = maxsize
+ self._keyboard_data: MutableMapping[str, _KeyboardData] = LRUCache(maxsize=maxsize)
+ self._callback_queries: MutableMapping[str, str] = LRUCache(maxsize=maxsize)
+ self.__lock = Lock()
+
+ if persistent_data:
+ keyboard_data, callback_queries = persistent_data
+ for key, value in callback_queries.items():
+ self._callback_queries[key] = value
+ for uuid, access_time, data in keyboard_data:
+ self._keyboard_data[uuid] = _KeyboardData(
+ keyboard_uuid=uuid, access_time=access_time, button_data=data
+ )
+
+ @property
+ def persistence_data(self) -> CDCData:
+ """:obj:`telegram.ext.utils.types.CDCData`: The data that needs to be persisted to allow
+ caching callback data across bot reboots.
+ """
+ # While building a list/dict from the LRUCaches has linear runtime (in the number of
+ # entries), the runtime is bounded by maxsize and it has the big upside of not throwing a
+ # highly customized data structure at users trying to implement a custom persistence class
+ with self.__lock:
+ return [data.to_tuple() for data in self._keyboard_data.values()], dict(
+ self._callback_queries.items()
+ )
+
+ def process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup:
+ """Registers the reply markup to the cache. If any of the buttons have
+ :attr:`callback_data`, stores that data and builds a new keyboard with the correspondingly
+ replaced buttons. Otherwise does nothing and returns the original reply markup.
+
+ Args:
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): The keyboard.
+
+ Returns:
+ :class:`telegram.InlineKeyboardMarkup`: The keyboard to be passed to Telegram.
+
+ """
+ with self.__lock:
+ return self.__process_keyboard(reply_markup)
+
+ def __process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup:
+ keyboard_uuid = uuid4().hex
+ keyboard_data = _KeyboardData(keyboard_uuid)
+
+ # Built a new nested list of buttons by replacing the callback data if needed
+ buttons = [
+ [
+ # We create a new button instead of replacing callback_data in case the
+ # same object is used elsewhere
+ InlineKeyboardButton(
+ btn.text,
+ callback_data=self.__put_button(btn.callback_data, keyboard_data),
+ )
+ if btn.callback_data
+ else btn
+ for btn in column
+ ]
+ for column in reply_markup.inline_keyboard
+ ]
+
+ if not keyboard_data.button_data:
+ # If we arrive here, no data had to be replaced and we can return the input
+ return reply_markup
+
+ self._keyboard_data[keyboard_uuid] = keyboard_data
+ return InlineKeyboardMarkup(buttons)
+
+ @staticmethod
+ def __put_button(callback_data: object, keyboard_data: _KeyboardData) -> str:
+ """Stores the data for a single button in :attr:`keyboard_data`.
+ Returns the string that should be passed instead of the callback_data, which is
+ ``keyboard_uuid + button_uuids``.
+ """
+ uuid = uuid4().hex
+ keyboard_data.button_data[uuid] = callback_data
+ return f'{keyboard_data.keyboard_uuid}{uuid}'
+
+ def __get_keyboard_uuid_and_button_data(
+ self, callback_data: str
+ ) -> Union[Tuple[str, object], Tuple[None, InvalidCallbackData]]:
+ keyboard, button = self.extract_uuids(callback_data)
+ try:
+ # we get the values before calling update() in case KeyErrors are raised
+ # we don't want to update in that case
+ keyboard_data = self._keyboard_data[keyboard]
+ button_data = keyboard_data.button_data[button]
+ # Update the timestamp for the LRU
+ keyboard_data.update_access_time()
+ return keyboard, button_data
+ except KeyError:
+ return None, InvalidCallbackData(callback_data)
+
+ @staticmethod
+ def extract_uuids(callback_data: str) -> Tuple[str, str]:
+ """Extracts the keyboard uuid and the button uuid from the given ``callback_data``.
+
+ Args:
+ callback_data (:obj:`str`): The ``callback_data`` as present in the button.
+
+ Returns:
+ (:obj:`str`, :obj:`str`): Tuple of keyboard and button uuid
+
+ """
+ # Extract the uuids as put in __put_button
+ return callback_data[:32], callback_data[32:]
+
+ def process_message(self, message: Message) -> None:
+ """Replaces the data in the inline keyboard attached to the message with the cached
+ objects, if necessary. If the data could not be found,
+ :class:`telegram.ext.InvalidCallbackData` will be inserted.
+
+ Note:
+ Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to check
+ if the reply markup (if any) was actually sent by this caches bot. If it was not, the
+ message will be returned unchanged.
+
+ Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is
+ :obj:`None` for those! In the corresponding reply markups the callback data will be
+ replaced by :class:`telegram.ext.InvalidCallbackData`.
+
+ Warning:
+ * Does *not* consider :attr:`telegram.Message.reply_to_message` and
+ :attr:`telegram.Message.pinned_message`. Pass them to these method separately.
+ * *In place*, i.e. the passed :class:`telegram.Message` will be changed!
+
+ Args:
+ message (:class:`telegram.Message`): The message.
+
+ """
+ with self.__lock:
+ self.__process_message(message)
+
+ def __process_message(self, message: Message) -> Optional[str]:
+ """As documented in process_message, but returns the uuid of the attached keyboard, if any,
+ which is relevant for process_callback_query.
+
+ **IN PLACE**
+ """
+ if not message.reply_markup:
+ return None
+
+ if message.via_bot:
+ sender: Optional[User] = message.via_bot
+ elif message.from_user:
+ sender = message.from_user
+ else:
+ sender = None
+
+ if sender is not None and sender != self.bot.bot:
+ return None
+
+ keyboard_uuid = None
+
+ for row in message.reply_markup.inline_keyboard:
+ for button in row:
+ if button.callback_data:
+ button_data = cast(str, button.callback_data)
+ keyboard_id, callback_data = self.__get_keyboard_uuid_and_button_data(
+ button_data
+ )
+ # update_callback_data makes sure that the _id_attrs are updated
+ button.update_callback_data(callback_data)
+
+ # This is lazy loaded. The firsts time we find a button
+ # we load the associated keyboard - afterwards, there is
+ if not keyboard_uuid and not isinstance(callback_data, InvalidCallbackData):
+ keyboard_uuid = keyboard_id
+
+ return keyboard_uuid
+
+ def process_callback_query(self, callback_query: CallbackQuery) -> None:
+ """Replaces the data in the callback query and the attached messages keyboard with the
+ cached objects, if necessary. If the data could not be found,
+ :class:`telegram.ext.InvalidCallbackData` will be inserted.
+ If :attr:`callback_query.data` or :attr:`callback_query.message` is present, this also
+ saves the callback queries ID in order to be able to resolve it to the stored data.
+
+ Note:
+ Also considers inserts data into the buttons of
+ :attr:`telegram.Message.reply_to_message` and :attr:`telegram.Message.pinned_message`
+ if necessary.
+
+ Warning:
+ *In place*, i.e. the passed :class:`telegram.CallbackQuery` will be changed!
+
+ Args:
+ callback_query (:class:`telegram.CallbackQuery`): The callback query.
+
+ """
+ with self.__lock:
+ mapped = False
+
+ if callback_query.data:
+ data = callback_query.data
+
+ # Get the cached callback data for the CallbackQuery
+ keyboard_uuid, button_data = self.__get_keyboard_uuid_and_button_data(data)
+ callback_query.data = button_data # type: ignore[assignment]
+
+ # Map the callback queries ID to the keyboards UUID for later use
+ if not mapped and not isinstance(button_data, InvalidCallbackData):
+ self._callback_queries[callback_query.id] = keyboard_uuid # type: ignore
+ mapped = True
+
+ # Get the cached callback data for the inline keyboard attached to the
+ # CallbackQuery.
+ if callback_query.message:
+ self.__process_message(callback_query.message)
+ for message in (
+ callback_query.message.pinned_message,
+ callback_query.message.reply_to_message,
+ ):
+ if message:
+ self.__process_message(message)
+
+ def drop_data(self, callback_query: CallbackQuery) -> None:
+ """Deletes the data for the specified callback query.
+
+ Note:
+ Will *not* raise exceptions in case the callback data is not found in the cache.
+ *Will* raise :class:`KeyError` in case the callback query can not be found in the
+ cache.
+
+ Args:
+ callback_query (:class:`telegram.CallbackQuery`): The callback query.
+
+ Raises:
+ KeyError: If the callback query can not be found in the cache
+ """
+ with self.__lock:
+ try:
+ keyboard_uuid = self._callback_queries.pop(callback_query.id)
+ self.__drop_keyboard(keyboard_uuid)
+ except KeyError as exc:
+ raise KeyError('CallbackQuery was not found in cache.') from exc
+
+ def __drop_keyboard(self, keyboard_uuid: str) -> None:
+ try:
+ self._keyboard_data.pop(keyboard_uuid)
+ except KeyError:
+ return
+
+ def clear_callback_data(self, time_cutoff: Union[float, datetime] = None) -> None:
+ """Clears the stored callback data.
+
+ Args:
+ time_cutoff (:obj:`float` | :obj:`datetime.datetime`, optional): Pass a UNIX timestamp
+ or a :obj:`datetime.datetime` to clear only entries which are older.
+ For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
+ bot will be used.
+
+ """
+ with self.__lock:
+ self.__clear(self._keyboard_data, time_cutoff=time_cutoff)
+
+ def clear_callback_queries(self) -> None:
+ """Clears the stored callback query IDs."""
+ with self.__lock:
+ self.__clear(self._callback_queries)
+
+ def __clear(self, mapping: MutableMapping, time_cutoff: Union[float, datetime] = None) -> None:
+ if not time_cutoff:
+ mapping.clear()
+ return
+
+ if isinstance(time_cutoff, datetime):
+ effective_cutoff = to_float_timestamp(
+ time_cutoff, tzinfo=self.bot.defaults.tzinfo if self.bot.defaults else None
+ )
+ else:
+ effective_cutoff = time_cutoff
+
+ # We need a list instead of a generator here, as the list doesn't change it's size
+ # during the iteration
+ to_drop = [key for key, data in mapping.items() if data.access_time < effective_cutoff]
+ for key in to_drop:
+ mapping.pop(key)
diff --git a/telegramer/include/telegram/ext/callbackqueryhandler.py b/telegramer/include/telegram/ext/callbackqueryhandler.py
index 3a8ca7a..bb19fa7 100644
--- a/telegramer/include/telegram/ext/callbackqueryhandler.py
+++ b/telegramer/include/telegram/ext/callbackqueryhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,134 +19,218 @@
"""This module contains the CallbackQueryHandler class."""
import re
-
-# REMREM from future.utils import string_types
-try:
- from future.utils import string_types
-except Exception as e:
- pass
-
-try:
- string_types
-except NameError:
- string_types = str
+from typing import (
+ TYPE_CHECKING,
+ Callable,
+ Dict,
+ Match,
+ Optional,
+ Pattern,
+ TypeVar,
+ Union,
+ cast,
+)
from telegram import Update
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+
from .handler import Handler
+from .utils.types import CCT
+
+if TYPE_CHECKING:
+ from telegram.ext import Dispatcher
+
+RT = TypeVar('RT')
-class CallbackQueryHandler(Handler):
+class CallbackQueryHandler(Handler[Update, CCT]):
"""Handler class to handle Telegram callback queries. Optionally based on a regex.
Read the documentation of the ``re`` module for more information.
- Attributes:
- callback (:obj:`callable`): The callback function for this handler.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
- pattern (:obj:`str` | `Pattern`): Optional. Regex pattern to test
- :attr:`telegram.CallbackQuery.data` against.
- pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the
- callback function.
- pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to
- the callback function.
- pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
- the callback function.
- pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
- the callback function.
-
Note:
- :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
- can use to keep any data in will be sent to the :attr:`callback` function. Related to
- either the user or the chat that the update was sent in. For each update from the same user
- or in the same chat, it will be the same ``dict``.
+ * :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
+ can use to keep any data in will be sent to the :attr:`callback` function. Related to
+ either the user or the chat that the update was sent in. For each update from the same
+ user or in the same chat, it will be the same ``dict``.
+
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+ * If your bot allows arbitrary objects as ``callback_data``, it may happen that the
+ original ``callback_data`` for the incoming :class:`telegram.CallbackQuery`` can not be
+ found. This is the case when either a malicious client tempered with the
+ ``callback_data`` or the data was simply dropped from cache or not persisted. In these
+ cases, an instance of :class:`telegram.ext.InvalidCallbackData` will be set as
+ ``callback_data``.
+
+ .. versionadded:: 13.6
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
- pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not ``None``, ``re.match``
- is used on :attr:`telegram.CallbackQuery.data` to determine if an update should be
- handled by this handler.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pattern (:obj:`str` | `Pattern` | :obj:`callable` | :obj:`type`, optional):
+ Pattern to test :attr:`telegram.CallbackQuery.data` against. If a string or a regex
+ pattern is passed, :meth:`re.match` is used on :attr:`telegram.CallbackQuery.data` to
+ determine if an update should be handled by this handler. If your bot allows arbitrary
+ objects as ``callback_data``, non-strings will be accepted. To filter arbitrary
+ objects you may pass
+
+ * a callable, accepting exactly one argument, namely the
+ :attr:`telegram.CallbackQuery.data`. It must return :obj:`True` or
+ :obj:`False`/:obj:`None` to indicate, whether the update should be handled.
+ * a :obj:`type`. If :attr:`telegram.CallbackQuery.data` is an instance of that type
+ (or a subclass), the update will be handled.
+
+ If :attr:`telegram.CallbackQuery.data` is :obj:`None`, the
+ :class:`telegram.CallbackQuery` update will not be handled.
+
+ .. versionchanged:: 13.6
+ Added support for arbitrary callback data.
pass_groups (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groups()`` as a keyword argument called ``groups``.
- Default is ``False``
+ Default is :obj:`False`
+ DEPRECATED: Please switch to context based callbacks.
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
- Default is ``False``
- pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``user_data`` will be passed to the callback function. Default is ``False``.
- pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``chat_data`` will be passed to the callback function. Default is ``False``.
+ Default is :obj:`False`
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pattern (`Pattern` | :obj:`callable` | :obj:`type`): Optional. Regex pattern, callback or
+ type to test :attr:`telegram.CallbackQuery.data` against.
+
+ .. versionchanged:: 13.6
+ Added support for arbitrary callback data.
+ pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the
+ callback function.
+ pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
"""
- def __init__(self,
- callback,
- pass_update_queue=False,
- pass_job_queue=False,
- pattern=None,
- pass_groups=False,
- pass_groupdict=False,
- pass_user_data=False,
- pass_chat_data=False):
- super(CallbackQueryHandler, self).__init__(
+ __slots__ = ('pattern', 'pass_groups', 'pass_groupdict')
+
+ def __init__(
+ self,
+ callback: Callable[[Update, CCT], RT],
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ pattern: Union[str, Pattern, type, Callable[[object], Optional[bool]]] = None,
+ pass_groups: bool = False,
+ pass_groupdict: bool = False,
+ pass_user_data: bool = False,
+ pass_chat_data: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
+ super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
- pass_chat_data=pass_chat_data)
+ pass_chat_data=pass_chat_data,
+ run_async=run_async,
+ )
- if isinstance(pattern, string_types):
+ if isinstance(pattern, str):
pattern = re.compile(pattern)
self.pattern = pattern
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
- def check_update(self, update):
+ def check_update(self, update: object) -> Optional[Union[bool, object]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:class:`telegram.Update`): Incoming telegram update.
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
if isinstance(update, Update) and update.callback_query:
+ callback_data = update.callback_query.data
if self.pattern:
- if update.callback_query.data:
- match = re.match(self.pattern, update.callback_query.data)
- return bool(match)
+ if callback_data is None:
+ return False
+ if isinstance(self.pattern, type):
+ return isinstance(callback_data, self.pattern)
+ if callable(self.pattern):
+ return self.pattern(callback_data)
+ match = re.match(self.pattern, callback_data)
+ if match:
+ return match
else:
return True
-
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
-
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
-
+ return None
+
+ def collect_optional_args(
+ self,
+ dispatcher: 'Dispatcher',
+ update: Update = None,
+ check_result: Union[bool, Match] = None,
+ ) -> Dict[str, object]:
+ """Pass the results of ``re.match(pattern, data).{groups(), groupdict()}`` to the
+ callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if
+ needed.
"""
- optional_args = self.collect_optional_args(dispatcher, update)
- if self.pattern:
- match = re.match(self.pattern, update.callback_query.data)
-
+ optional_args = super().collect_optional_args(dispatcher, update, check_result)
+ if self.pattern and not callable(self.pattern):
+ check_result = cast(Match, check_result)
if self.pass_groups:
- optional_args['groups'] = match.groups()
+ optional_args['groups'] = check_result.groups()
if self.pass_groupdict:
- optional_args['groupdict'] = match.groupdict()
-
- return self.callback(dispatcher.bot, update, **optional_args)
+ optional_args['groupdict'] = check_result.groupdict()
+ return optional_args
+
+ def collect_additional_context(
+ self,
+ context: CCT,
+ update: Update,
+ dispatcher: 'Dispatcher',
+ check_result: Union[bool, Match],
+ ) -> None:
+ """Add the result of ``re.match(pattern, update.callback_query.data)`` to
+ :attr:`CallbackContext.matches` as list with one element.
+ """
+ if self.pattern:
+ check_result = cast(Match, check_result)
+ context.matches = [check_result]
diff --git a/telegramer/include/telegram/ext/chatjoinrequesthandler.py b/telegramer/include/telegram/ext/chatjoinrequesthandler.py
new file mode 100644
index 0000000..aafed54
--- /dev/null
+++ b/telegramer/include/telegram/ext/chatjoinrequesthandler.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains the ChatJoinRequestHandler class."""
+
+
+from telegram import Update
+
+from .handler import Handler
+from .utils.types import CCT
+
+
+class ChatJoinRequestHandler(Handler[Update, CCT]):
+ """Handler class to handle Telegram updates that contain a chat join request.
+
+ Note:
+ :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
+ can use to keep any data in will be sent to the :attr:`callback` function. Related to
+ either the user or the chat that the update was sent in. For each update from the same user
+ or in the same chat, it will be the same ``dict``.
+
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
+ .. versionadded:: 13.8
+
+ Args:
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``update_queue`` will be passed to the callback function. It will be the ``Queue``
+ instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``job_queue`` will be passed to the callback function. It will be a
+ :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+
+ """
+
+ __slots__ = ()
+
+ def check_update(self, update: object) -> bool:
+ """Determines whether an update should be passed to this handlers :attr:`callback`.
+
+ Args:
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
+
+ Returns:
+ :obj:`bool`
+
+ """
+ return isinstance(update, Update) and bool(update.chat_join_request)
diff --git a/telegramer/include/telegram/ext/chatmemberhandler.py b/telegramer/include/telegram/ext/chatmemberhandler.py
new file mode 100644
index 0000000..eb9d91b
--- /dev/null
+++ b/telegramer/include/telegram/ext/chatmemberhandler.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains the ChatMemberHandler classes."""
+from typing import ClassVar, TypeVar, Union, Callable
+
+from telegram import Update
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+from .handler import Handler
+from .utils.types import CCT
+
+RT = TypeVar('RT')
+
+
+class ChatMemberHandler(Handler[Update, CCT]):
+ """Handler class to handle Telegram updates that contain a chat member update.
+
+ .. versionadded:: 13.4
+
+ Note:
+ :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
+ can use to keep any data in will be sent to the :attr:`callback` function. Related to
+ either the user or the chat that the update was sent in. For each update from the same user
+ or in the same chat, it will be the same ``dict``.
+
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
+ Args:
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ chat_member_types (:obj:`int`, optional): Pass one of :attr:`MY_CHAT_MEMBER`,
+ :attr:`CHAT_MEMBER` or :attr:`ANY_CHAT_MEMBER` to specify if this handler should handle
+ only updates with :attr:`telegram.Update.my_chat_member`,
+ :attr:`telegram.Update.chat_member` or both. Defaults to :attr:`MY_CHAT_MEMBER`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``update_queue`` will be passed to the callback function. It will be the ``Queue``
+ instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``job_queue`` will be passed to the callback function. It will be a
+ :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ chat_member_types (:obj:`int`, optional): Specifies if this handler should handle
+ only updates with :attr:`telegram.Update.my_chat_member`,
+ :attr:`telegram.Update.chat_member` or both.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+
+ """
+
+ __slots__ = ('chat_member_types',)
+ MY_CHAT_MEMBER: ClassVar[int] = -1
+ """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.my_chat_member`."""
+ CHAT_MEMBER: ClassVar[int] = 0
+ """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.chat_member`."""
+ ANY_CHAT_MEMBER: ClassVar[int] = 1
+ """:obj:`int`: Used as a constant to handle bot :attr:`telegram.Update.my_chat_member`
+ and :attr:`telegram.Update.chat_member`."""
+
+ def __init__(
+ self,
+ callback: Callable[[Update, CCT], RT],
+ chat_member_types: int = MY_CHAT_MEMBER,
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ pass_user_data: bool = False,
+ pass_chat_data: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
+ super().__init__(
+ callback,
+ pass_update_queue=pass_update_queue,
+ pass_job_queue=pass_job_queue,
+ pass_user_data=pass_user_data,
+ pass_chat_data=pass_chat_data,
+ run_async=run_async,
+ )
+
+ self.chat_member_types = chat_member_types
+
+ def check_update(self, update: object) -> bool:
+ """Determines whether an update should be passed to this handlers :attr:`callback`.
+
+ Args:
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
+
+ Returns:
+ :obj:`bool`
+
+ """
+ if isinstance(update, Update):
+ if not (update.my_chat_member or update.chat_member):
+ return False
+ if self.chat_member_types == self.ANY_CHAT_MEMBER:
+ return True
+ if self.chat_member_types == self.CHAT_MEMBER:
+ return bool(update.chat_member)
+ return bool(update.my_chat_member)
+ return False
diff --git a/telegramer/include/telegram/ext/choseninlineresulthandler.py b/telegramer/include/telegram/ext/choseninlineresulthandler.py
index 0b02456..1d94b79 100644
--- a/telegramer/include/telegram/ext/choseninlineresulthandler.py
+++ b/telegramer/include/telegram/ext/choseninlineresulthandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,25 +17,23 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the ChosenInlineResultHandler class."""
+import re
+from typing import Optional, TypeVar, Union, Callable, TYPE_CHECKING, Pattern, Match, cast
-from .handler import Handler
from telegram import Update
-from telegram.utils.deprecate import deprecate
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+from .handler import Handler
+from .utils.types import CCT
-class ChosenInlineResultHandler(Handler):
- """Handler class to handle Telegram updates that contain a chosen inline result.
+RT = TypeVar('RT')
- Attributes:
- callback (:obj:`callable`): The callback function for this handler.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
- pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
- the callback function.
- pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
- the callback function.
+if TYPE_CHECKING:
+ from telegram.ext import CallbackContext, Dispatcher
+
+
+class ChosenInlineResultHandler(Handler[Update, CCT]):
+ """Handler class to handle Telegram updates that contain a chosen inline result.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -43,63 +41,120 @@ class ChosenInlineResultHandler(Handler):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
Args:
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
- pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``user_data`` will be passed to the callback function. Default is ``False``.
- pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``chat_data`` will be passed to the callback function. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+ pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not :obj:`None`, ``re.match``
+ is used on :attr:`telegram.ChosenInlineResult.result_id` to determine if an update
+ should be handled by this handler. This is accessible in the callback as
+ :attr:`telegram.ext.CallbackContext.matches`.
+
+ .. versionadded:: 13.6
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ pattern (`Pattern`): Optional. Regex pattern to test
+ :attr:`telegram.ChosenInlineResult.result_id` against.
+
+ .. versionadded:: 13.6
"""
- def __init__(self,
- callback,
- pass_update_queue=False,
- pass_job_queue=False,
- pass_user_data=False,
- pass_chat_data=False):
- super(ChosenInlineResultHandler, self).__init__(
+ __slots__ = ('pattern',)
+
+ def __init__(
+ self,
+ callback: Callable[[Update, 'CallbackContext'], RT],
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ pass_user_data: bool = False,
+ pass_chat_data: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ pattern: Union[str, Pattern] = None,
+ ):
+ super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
- pass_chat_data=pass_chat_data)
+ pass_chat_data=pass_chat_data,
+ run_async=run_async,
+ )
- def check_update(self, update):
+ if isinstance(pattern, str):
+ pattern = re.compile(pattern)
+
+ self.pattern = pattern
+
+ def check_update(self, update: object) -> Optional[Union[bool, object]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:class:`telegram.Update`): Incoming telegram update.
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
- return isinstance(update, Update) and update.chosen_inline_result
-
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
-
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
-
+ if isinstance(update, Update) and update.chosen_inline_result:
+ if self.pattern:
+ match = re.match(self.pattern, update.chosen_inline_result.result_id)
+ if match:
+ return match
+ else:
+ return True
+ return None
+
+ def collect_additional_context(
+ self,
+ context: 'CallbackContext',
+ update: Update,
+ dispatcher: 'Dispatcher',
+ check_result: Union[bool, Match],
+ ) -> None:
+ """This function adds the matched regex pattern result to
+ :attr:`telegram.ext.CallbackContext.matches`.
"""
- optional_args = self.collect_optional_args(dispatcher, update)
-
- return self.callback(dispatcher.bot, update, **optional_args)
-
- # old non-PEP8 Handler methods
- m = "telegram.ChosenInlineResultHandler."
- checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
- handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")
+ if self.pattern:
+ check_result = cast(Match, check_result)
+ context.matches = [check_result]
diff --git a/telegramer/include/telegram/ext/commandhandler.py b/telegramer/include/telegram/ext/commandhandler.py
index 5ee5c62..6f53d23 100644
--- a/telegramer/include/telegram/ext/commandhandler.py
+++ b/telegramer/include/telegram/ext/commandhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,167 +16,441 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
-"""This module contains the CommandHandler class."""
+"""This module contains the CommandHandler and PrefixHandler classes."""
+import re
import warnings
+from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, TypeVar, Union
-# REMREM from future.utils import string_types
-try:
- from future.utils import string_types
-except Exception as e:
- pass
-
-try:
- string_types
-except NameError:
- string_types = str
+from telegram import MessageEntity, Update
+from telegram.ext import BaseFilter, Filters
+from telegram.utils.deprecate import TelegramDeprecationWarning
+from telegram.utils.types import SLT
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+from .utils.types import CCT
from .handler import Handler
-from telegram import Update
+if TYPE_CHECKING:
+ from telegram.ext import Dispatcher
+
+RT = TypeVar('RT')
-class CommandHandler(Handler):
+
+class CommandHandler(Handler[Update, CCT]):
"""Handler class to handle Telegram commands.
Commands are Telegram messages that start with ``/``, optionally followed by an ``@`` and the
- bot's name and/or some additional text.
+ bot's name and/or some additional text. The handler will add a ``list`` to the
+ :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings,
+ which is the text following the command split on single or consecutive whitespace characters.
- Attributes:
- command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler
- should listen for.
- callback (:obj:`callable`): The callback function for this handler.
- filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these
- Filters.
- allow_edited (:obj:`bool`): Optional. Determines Whether the handler should also accept
- edited messages.
- pass_args (:obj:`bool`): Optional. Determines whether the handler should be passed
- ``args``.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
- pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
- the callback function.
- pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
- the callback function.
+ By default the handler listens to messages as well as edited messages. To change this behavior
+ use ``~Filters.update.edited_message`` in the filter argument.
Note:
- :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
- can use to keep any data in will be sent to the :attr:`callback` function. Related to
- either the user or the chat that the update was sent in. For each update from the same user
- or in the same chat, it will be the same ``dict``.
+ * :class:`CommandHandler` does *not* handle (edited) channel posts.
+ * :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a :obj:`dict` you
+ can use to keep any data in will be sent to the :attr:`callback` function. Related to
+ either the user or the chat that the update was sent in. For each update from the same
+ user or in the same chat, it will be the same :obj:`dict`.
+
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
- command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler
- should listen for.
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
+ command (:class:`telegram.utils.types.SLT[str]`):
+ The command or list of commands this handler should listen for.
+ Limitations are the same as described here https://core.telegram.org/bots#commands
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from
:class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in
:class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise
operators (& for and, | for or, ~ for not).
allow_edited (:obj:`bool`, optional): Determines whether the handler should also accept
- edited messages. Default is ``False``.
+ edited messages. Default is :obj:`False`.
+ DEPRECATED: Edited is allowed by default. To change this behavior use
+ ``~Filters.update.edited_message``.
pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the
arguments passed to the command as a keyword argument called ``args``. It will contain
a list of strings, which is the text following the command split on single or
- consecutive whitespace characters. Default is ``False``
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ consecutive whitespace characters. Default is :obj:`False`
+ DEPRECATED: Please switch to context based callbacks.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
- pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``user_data`` will be passed to the callback function. Default is ``False``.
- pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``chat_data`` will be passed to the callback function. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Raises:
+ ValueError: when command is too long or has illegal chars.
+ Attributes:
+ command (:class:`telegram.utils.types.SLT[str]`):
+ The command or list of commands this handler should listen for.
+ Limitations are the same as described here https://core.telegram.org/bots#commands
+ callback (:obj:`callable`): The callback function for this handler.
+ filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these
+ Filters.
+ allow_edited (:obj:`bool`): Determines whether the handler should also accept
+ edited messages.
+ pass_args (:obj:`bool`): Determines whether the handler should be passed
+ ``args``.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
"""
- def __init__(self,
- command,
- callback,
- filters=None,
- allow_edited=False,
- pass_args=False,
- pass_update_queue=False,
- pass_job_queue=False,
- pass_user_data=False,
- pass_chat_data=False):
- super(CommandHandler, self).__init__(
+ __slots__ = ('command', 'filters', 'pass_args')
+
+ def __init__(
+ self,
+ command: SLT[str],
+ callback: Callable[[Update, CCT], RT],
+ filters: BaseFilter = None,
+ allow_edited: bool = None,
+ pass_args: bool = False,
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ pass_user_data: bool = False,
+ pass_chat_data: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
+ super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
- pass_chat_data=pass_chat_data)
+ pass_chat_data=pass_chat_data,
+ run_async=run_async,
+ )
- if isinstance(command, string_types):
+ if isinstance(command, str):
self.command = [command.lower()]
else:
self.command = [x.lower() for x in command]
- self.filters = filters
- self.allow_edited = allow_edited
- self.pass_args = pass_args
+ for comm in self.command:
+ if not re.match(r'^[\da-z_]{1,32}$', comm):
+ raise ValueError('Command is not a valid bot command')
- # We put this up here instead of with the rest of checking code
- # in check_update since we don't wanna spam a ton
- if isinstance(self.filters, list):
- warnings.warn('Using a list of filters in MessageHandler is getting '
- 'deprecated, please use bitwise operators (& and |) '
- 'instead. More info: https://git.io/vPTbc.')
+ if filters:
+ self.filters = Filters.update.messages & filters
+ else:
+ self.filters = Filters.update.messages
- def check_update(self, update):
+ if allow_edited is not None:
+ warnings.warn(
+ 'allow_edited is deprecated. See https://git.io/fxJuV for more info',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+ if not allow_edited:
+ self.filters &= ~Filters.update.edited_message
+ self.pass_args = pass_args
+
+ def check_update(
+ self, update: object
+ ) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, Dict]]]]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:class:`telegram.Update`): Incoming telegram update.
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
- :obj:`bool`
+ :obj:`list`: The list of args for the handler.
+
+ """
+ if isinstance(update, Update) and update.effective_message:
+ message = update.effective_message
+
+ if (
+ message.entities
+ and message.entities[0].type == MessageEntity.BOT_COMMAND
+ and message.entities[0].offset == 0
+ and message.text
+ and message.bot
+ ):
+ command = message.text[1 : message.entities[0].length]
+ args = message.text.split()[1:]
+ command_parts = command.split('@')
+ command_parts.append(message.bot.username)
+
+ if not (
+ command_parts[0].lower() in self.command
+ and command_parts[1].lower() == message.bot.username.lower()
+ ):
+ return None
+
+ filter_result = self.filters(update)
+ if filter_result:
+ return args, filter_result
+ return False
+ return None
+ def collect_optional_args(
+ self,
+ dispatcher: 'Dispatcher',
+ update: Update = None,
+ check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]] = None,
+ ) -> Dict[str, object]:
+ """Provide text after the command to the callback the ``args`` argument as list, split on
+ single whitespaces.
"""
- if (isinstance(update, Update)
- and (update.message or update.edited_message and self.allow_edited)):
- message = update.message or update.edited_message
+ optional_args = super().collect_optional_args(dispatcher, update)
+ if self.pass_args and isinstance(check_result, tuple):
+ optional_args['args'] = check_result[0]
+ return optional_args
- if message.text and message.text.startswith('/') and len(message.text) > 1:
- first_word = message.text_html.split(None, 1)[0]
- if len(first_word) > 1 and first_word.startswith('/'):
- command = first_word[1:].split('@')
- command.append(
- message.bot.username) # in case the command was sent without a username
+ def collect_additional_context(
+ self,
+ context: CCT,
+ update: Update,
+ dispatcher: 'Dispatcher',
+ check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]],
+ ) -> None:
+ """Add text after the command to :attr:`CallbackContext.args` as list, split on single
+ whitespaces and add output of data filters to :attr:`CallbackContext` as well.
+ """
+ if isinstance(check_result, tuple):
+ context.args = check_result[0]
+ if isinstance(check_result[1], dict):
+ context.update(check_result[1])
- if not (command[0].lower() in self.command
- and command[1].lower() == message.bot.username.lower()):
- return False
- if self.filters is None:
- res = True
- elif isinstance(self.filters, list):
- res = any(func(message) for func in self.filters)
- else:
- res = self.filters(message)
+class PrefixHandler(CommandHandler):
+ """Handler class to handle custom prefix commands.
- return res
+ This is a intermediate handler between :class:`MessageHandler` and :class:`CommandHandler`.
+ It supports configurable commands with the same options as CommandHandler. It will respond to
+ every combination of :attr:`prefix` and :attr:`command`. It will add a ``list`` to the
+ :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings,
+ which is the text following the command split on single or consecutive whitespace characters.
- return False
+ Examples:
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
+ Single prefix and command:
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
+ .. code:: python
+
+ PrefixHandler('!', 'test', callback) # will respond to '!test'.
+
+ Multiple prefixes, single command:
+
+ .. code:: python
+
+ PrefixHandler(['!', '#'], 'test', callback) # will respond to '!test' and '#test'.
+
+ Multiple prefixes and commands:
+
+ .. code:: python
+
+ PrefixHandler(['!', '#'], ['test', 'help'], callback) # will respond to '!test', \
+ '#test', '!help' and '#help'.
+
+
+ By default the handler listens to messages as well as edited messages. To change this behavior
+ use ``~Filters.update.edited_message``.
+
+ Note:
+ * :class:`PrefixHandler` does *not* handle (edited) channel posts.
+ * :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a :obj:`dict` you
+ can use to keep any data in will be sent to the :attr:`callback` function. Related to
+ either the user or the chat that the update was sent in. For each update from the same
+ user or in the same chat, it will be the same :obj:`dict`.
+
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
+ Args:
+ prefix (:class:`telegram.utils.types.SLT[str]`):
+ The prefix(es) that will precede :attr:`command`.
+ command (:class:`telegram.utils.types.SLT[str]`):
+ The command or list of commands this handler should listen for.
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from
+ :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in
+ :class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise
+ operators (& for and, | for or, ~ for not).
+ pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the
+ arguments passed to the command as a keyword argument called ``args``. It will contain
+ a list of strings, which is the text following the command split on single or
+ consecutive whitespace characters. Default is :obj:`False`
+ DEPRECATED: Please switch to context based callbacks.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``update_queue`` will be passed to the callback function. It will be the ``Queue``
+ instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``job_queue`` will be passed to the callback function. It will be a
+ :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these
+ Filters.
+ pass_args (:obj:`bool`): Determines whether the handler should be passed
+ ``args``.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+
+ """
+
+ # 'prefix' is a class property, & 'command' is included in the superclass, so they're left out.
+ __slots__ = ('_prefix', '_command', '_commands')
+ def __init__(
+ self,
+ prefix: SLT[str],
+ command: SLT[str],
+ callback: Callable[[Update, CCT], RT],
+ filters: BaseFilter = None,
+ pass_args: bool = False,
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ pass_user_data: bool = False,
+ pass_chat_data: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
+
+ self._prefix: List[str] = []
+ self._command: List[str] = []
+ self._commands: List[str] = []
+
+ super().__init__(
+ 'nocommand',
+ callback,
+ filters=filters,
+ allow_edited=None,
+ pass_args=pass_args,
+ pass_update_queue=pass_update_queue,
+ pass_job_queue=pass_job_queue,
+ pass_user_data=pass_user_data,
+ pass_chat_data=pass_chat_data,
+ run_async=run_async,
+ )
+
+ self.prefix = prefix # type: ignore[assignment]
+ self.command = command # type: ignore[assignment]
+ self._build_commands()
+
+ @property
+ def prefix(self) -> List[str]:
+ """
+ The prefixes that will precede :attr:`command`.
+
+ Returns:
+ List[:obj:`str`]
"""
- optional_args = self.collect_optional_args(dispatcher, update)
+ return self._prefix
- message = update.message or update.edited_message
+ @prefix.setter
+ def prefix(self, prefix: Union[str, List[str]]) -> None:
+ if isinstance(prefix, str):
+ self._prefix = [prefix.lower()]
+ else:
+ self._prefix = prefix
+ self._build_commands()
+
+ @property # type: ignore[override]
+ def command(self) -> List[str]: # type: ignore[override]
+ """
+ The list of commands this handler should listen for.
- if self.pass_args:
- optional_args['args'] = message.text.split()[1:]
+ Returns:
+ List[:obj:`str`]
+ """
+ return self._command
+
+ @command.setter
+ def command(self, command: Union[str, List[str]]) -> None:
+ if isinstance(command, str):
+ self._command = [command.lower()]
+ else:
+ self._command = command
+ self._build_commands()
+
+ def _build_commands(self) -> None:
+ self._commands = [x.lower() + y.lower() for x in self.prefix for y in self.command]
+
+ def check_update(
+ self, update: object
+ ) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, Dict]]]]]:
+ """Determines whether an update should be passed to this handlers :attr:`callback`.
+
+ Args:
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
+
+ Returns:
+ :obj:`list`: The list of args for the handler.
+
+ """
+ if isinstance(update, Update) and update.effective_message:
+ message = update.effective_message
- return self.callback(dispatcher.bot, update, **optional_args)
+ if message.text:
+ text_list = message.text.split()
+ if text_list[0].lower() not in self._commands:
+ return None
+ filter_result = self.filters(update)
+ if filter_result:
+ return text_list[1:], filter_result
+ return False
+ return None
diff --git a/telegramer/include/telegram/ext/contexttypes.py b/telegramer/include/telegram/ext/contexttypes.py
new file mode 100644
index 0000000..ee03037
--- /dev/null
+++ b/telegramer/include/telegram/ext/contexttypes.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=R0201
+"""This module contains the auxiliary class ContextTypes."""
+from typing import Type, Generic, overload, Dict # pylint: disable=W0611
+
+from telegram.ext.callbackcontext import CallbackContext
+from telegram.ext.utils.types import CCT, UD, CD, BD
+
+
+class ContextTypes(Generic[CCT, UD, CD, BD]):
+ """
+ Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext`
+ interface.
+
+ .. versionadded:: 13.6
+
+ Args:
+ context (:obj:`type`, optional): Determines the type of the ``context`` argument of all
+ (error-)handler callbacks and job callbacks. Must be a subclass of
+ :class:`telegram.ext.CallbackContext`. Defaults to
+ :class:`telegram.ext.CallbackContext`.
+ bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data`` of all
+ (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support
+ instantiating without arguments.
+ chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data`` of all
+ (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support
+ instantiating without arguments.
+ user_data (:obj:`type`, optional): Determines the type of ``context.user_data`` of all
+ (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support
+ instantiating without arguments.
+
+ """
+
+ __slots__ = ('_context', '_bot_data', '_chat_data', '_user_data')
+
+ # overload signatures generated with https://git.io/JtJPj
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]',
+ ):
+ ...
+
+ @overload
+ def __init__(self: 'ContextTypes[CCT, Dict, Dict, Dict]', context: Type[CCT]):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CallbackContext[UD, Dict, Dict], UD, Dict, Dict]', user_data: Type[UD]
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CallbackContext[Dict, CD, Dict], Dict, CD, Dict]', chat_data: Type[CD]
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CallbackContext[Dict, Dict, BD], Dict, Dict, BD]', bot_data: Type[BD]
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CCT, UD, Dict, Dict]', context: Type[CCT], user_data: Type[UD]
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CCT, Dict, CD, Dict]', context: Type[CCT], chat_data: Type[CD]
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CCT, Dict, Dict, BD]', context: Type[CCT], bot_data: Type[BD]
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CallbackContext[UD, CD, Dict], UD, CD, Dict]',
+ user_data: Type[UD],
+ chat_data: Type[CD],
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CallbackContext[UD, Dict, BD], UD, Dict, BD]',
+ user_data: Type[UD],
+ bot_data: Type[BD],
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CallbackContext[Dict, CD, BD], Dict, CD, BD]',
+ chat_data: Type[CD],
+ bot_data: Type[BD],
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CCT, UD, CD, Dict]',
+ context: Type[CCT],
+ user_data: Type[UD],
+ chat_data: Type[CD],
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CCT, UD, Dict, BD]',
+ context: Type[CCT],
+ user_data: Type[UD],
+ bot_data: Type[BD],
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CCT, Dict, CD, BD]',
+ context: Type[CCT],
+ chat_data: Type[CD],
+ bot_data: Type[BD],
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CallbackContext[UD, CD, BD], UD, CD, BD]',
+ user_data: Type[UD],
+ chat_data: Type[CD],
+ bot_data: Type[BD],
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'ContextTypes[CCT, UD, CD, BD]',
+ context: Type[CCT],
+ user_data: Type[UD],
+ chat_data: Type[CD],
+ bot_data: Type[BD],
+ ):
+ ...
+
+ def __init__( # type: ignore[no-untyped-def]
+ self,
+ context=CallbackContext,
+ bot_data=dict,
+ chat_data=dict,
+ user_data=dict,
+ ):
+ if not issubclass(context, CallbackContext):
+ raise ValueError('context must be a subclass of CallbackContext.')
+
+ # We make all those only accessible via properties because we don't currently support
+ # changing this at runtime, so overriding the attributes doesn't make sense
+ self._context = context
+ self._bot_data = bot_data
+ self._chat_data = chat_data
+ self._user_data = user_data
+
+ @property
+ def context(self) -> Type[CCT]:
+ return self._context
+
+ @property
+ def bot_data(self) -> Type[BD]:
+ return self._bot_data
+
+ @property
+ def chat_data(self) -> Type[CD]:
+ return self._chat_data
+
+ @property
+ def user_data(self) -> Type[UD]:
+ return self._user_data
diff --git a/telegramer/include/telegram/ext/conversationhandler.py b/telegramer/include/telegram/ext/conversationhandler.py
index 0cc6633..23edf2f 100644
--- a/telegramer/include/telegram/ext/conversationhandler.py
+++ b/telegramer/include/telegram/ext/conversationhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,30 +16,81 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=R0201
"""This module contains the ConversationHandler."""
import logging
+import warnings
+import functools
+import datetime
+from threading import Lock
+from typing import TYPE_CHECKING, Dict, List, NoReturn, Optional, Union, Tuple, cast, ClassVar
from telegram import Update
-from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler,
- ChosenInlineResultHandler)
-from telegram.utils.promise import Promise
+from telegram.ext import (
+ BasePersistence,
+ CallbackContext,
+ CallbackQueryHandler,
+ ChosenInlineResultHandler,
+ DispatcherHandlerStop,
+ Handler,
+ InlineQueryHandler,
+)
+from telegram.ext.utils.promise import Promise
+from telegram.ext.utils.types import ConversationDict
+from telegram.ext.utils.types import CCT
+
+if TYPE_CHECKING:
+ from telegram.ext import Dispatcher, Job
+CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]]
+
+
+class _ConversationTimeoutContext:
+ # '__dict__' is not included since this a private class
+ __slots__ = ('conversation_key', 'update', 'dispatcher', 'callback_context')
+
+ def __init__(
+ self,
+ conversation_key: Tuple[int, ...],
+ update: Update,
+ dispatcher: 'Dispatcher',
+ callback_context: Optional[CallbackContext],
+ ):
+ self.conversation_key = conversation_key
+ self.update = update
+ self.dispatcher = dispatcher
+ self.callback_context = callback_context
+
+
+class ConversationHandler(Handler[Update, CCT]):
+ """
+ A handler to hold a conversation with a single or multiple users through Telegram updates by
+ managing four collections of other handlers.
+ Note:
+ ``ConversationHandler`` will only accept updates that are (subclass-)instances of
+ :class:`telegram.Update`. This is, because depending on the :attr:`per_user` and
+ :attr:`per_chat` ``ConversationHandler`` relies on
+ :attr:`telegram.Update.effective_user` and/or :attr:`telegram.Update.effective_chat` in
+ order to determine which conversation an update should belong to. For ``per_message=True``,
+ ``ConversationHandler`` uses ``update.callback_query.message.message_id`` when
+ ``per_chat=True`` and ``update.callback_query.inline_message_id`` when ``per_chat=False``.
+ For a more detailed explanation, please see our `FAQ`_.
-class ConversationHandler(Handler):
- """
- A handler to hold a conversation with a single user by managing four collections of other
- handlers. Note that neither posts in Telegram Channels, nor group interactions with multiple
- users are managed by instances of this class.
+ Finally, ``ConversationHandler``, does *not* handle (edited) channel posts.
+
+ .. _`FAQ`: https://git.io/JtcyU
The first collection, a ``list`` named :attr:`entry_points`, is used to initiate the
conversation, for example with a :class:`telegram.ext.CommandHandler` or
- :class:`telegram.ext.RegexHandler`.
+ :class:`telegram.ext.MessageHandler`.
The second collection, a ``dict`` named :attr:`states`, contains the different conversation
steps and one or more associated handlers that should be used if the user sends a message when
- the conversation with them is currently in that state. You will probably use mostly
- :class:`telegram.ext.MessageHandler` and :class:`telegram.ext.RegexHandler` here.
+ the conversation with them is currently in that state. Here you can also define a state for
+ :attr:`TIMEOUT` to define the behavior when :attr:`conversation_timeout` is exceeded, and a
+ state for :attr:`WAITING` to define behavior when a new update is received while the previous
+ ``@run_async`` decorated handler is not finished.
The third collection, a ``list`` named :attr:`fallbacks`, is used if the user is currently in a
conversation but the state has either no associated handler or the handler that is associated
@@ -47,108 +98,161 @@ class ConversationHandler(Handler):
a regular text message is expected. You could use this for a ``/cancel`` command or to let the
user know their message was not recognized.
- The fourth, optional collection of handlers, a ``list`` named :attr:`timed_out_behavior` is
- used if the wait for ``run_async`` takes longer than defined in :attr:`run_async_timeout`.
- For example, you can let the user know that they should wait for a bit before they can
- continue.
-
To change the state of conversation, the callback function of a handler must return the new
- state after responding to the user. If it does not return anything (returning ``None`` by
- default), the state will not change. To end the conversation, the callback function must
- return :attr:`END` or ``-1``.
-
- Attributes:
- entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
- trigger the start of the conversation.
- states (Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]): A :obj:`dict` that
- defines the different states of conversation a user can be in and one or more
- associated ``Handler`` objects that should be used in that state.
- fallbacks (List[:class:`telegram.ext.Handler`]): A list of handlers that might be used if
- the user is in a conversation, but every handler for their current state returned
- ``False`` on :attr:`check_update`.
- allow_reentry (:obj:`bool`): Optional. Determines if a user can restart a conversation with
- an entry point.
- run_async_timeout (:obj:`float`): Optional. The time-out for ``run_async`` decorated
- Handlers.
- timed_out_behavior (List[:class:`telegram.ext.Handler`]): Optional. A list of handlers that
- might be used if the wait for ``run_async`` timed out.
- per_chat (:obj:`bool`): Optional. If the conversationkey should contain the Chat's ID.
- per_user (:obj:`bool`): Optional. If the conversationkey should contain the User's ID.
- per_message (:obj:`bool`): Optional. If the conversationkey should contain the Message's
- ID.
- conversation_timeout (:obj:`float`|:obj:`datetime.timedelta`): Optional. When this handler
- is inactive more than this timeout (in seconds), it will be automatically ended. If
- this value is 0 (default), there will be no timeout.
+ state after responding to the user. If it does not return anything (returning :obj:`None` by
+ default), the state will not change. If an entry point callback function returns :obj:`None`,
+ the conversation ends immediately after the execution of this callback function.
+ To end the conversation, the callback function must return :attr:`END` or ``-1``. To
+ handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``.
+ Finally, :class:`telegram.ext.DispatcherHandlerStop` can be used in conversations as described
+ in the corresponding documentation.
+
+ Note:
+ In each of the described collections of handlers, a handler may in turn be a
+ :class:`ConversationHandler`. In that case, the nested :class:`ConversationHandler` should
+ have the attribute :attr:`map_to_parent` which allows to return to the parent conversation
+ at specified states within the nested conversation.
+
+ Note that the keys in :attr:`map_to_parent` must not appear as keys in :attr:`states`
+ attribute or else the latter will be ignored. You may map :attr:`END` to one of the parents
+ states to continue the parent conversation after this has ended or even map a state to
+ :attr:`END` to end the *parent* conversation from within the nested one. For an example on
+ nested :class:`ConversationHandler` s, see our `examples`_.
+
+ .. _`examples`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples
Args:
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
trigger the start of the conversation. The first handler which :attr:`check_update`
- method returns ``True`` will be used. If all return ``False``, the update is not
+ method returns :obj:`True` will be used. If all return :obj:`False`, the update is not
handled.
states (Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]): A :obj:`dict` that
defines the different states of conversation a user can be in and one or more
associated ``Handler`` objects that should be used in that state. The first handler
- which :attr:`check_update` method returns ``True`` will be used.
+ which :attr:`check_update` method returns :obj:`True` will be used.
fallbacks (List[:class:`telegram.ext.Handler`]): A list of handlers that might be used if
the user is in a conversation, but every handler for their current state returned
- ``False`` on :attr:`check_update`. The first handler which :attr:`check_update` method
- returns ``True`` will be used. If all return ``False``, the update is not handled.
- allow_reentry (:obj:`bool`, optional): If set to ``True``, a user that is currently in a
+ :obj:`False` on :attr:`check_update`. The first handler which :attr:`check_update`
+ method returns :obj:`True` will be used. If all return :obj:`False`, the update is not
+ handled.
+ allow_reentry (:obj:`bool`, optional): If set to :obj:`True`, a user that is currently in a
conversation can restart the conversation by triggering one of the entry points.
- run_async_timeout (:obj:`float`, optional): If the previous handler for this user was
- running asynchronously using the ``run_async`` decorator, it might not be finished when
- the next message arrives. This timeout defines how long the conversation handler should
- wait for the next state to be computed. The default is ``None`` which means it will
- wait indefinitely.
- timed_out_behavior (List[:class:`telegram.ext.Handler`], optional): A list of handlers that
- might be used if the wait for ``run_async`` timed out. The first handler which
- :attr:`check_update` method returns ``True`` will be used. If all return ``False``,
- the update is not handled.
per_chat (:obj:`bool`, optional): If the conversationkey should contain the Chat's ID.
- Default is ``True``.
+ Default is :obj:`True`.
per_user (:obj:`bool`, optional): If the conversationkey should contain the User's ID.
- Default is ``True``.
+ Default is :obj:`True`.
per_message (:obj:`bool`, optional): If the conversationkey should contain the Message's
- ID. Default is ``False``.
- conversation_timeout (:obj:`float`|:obj:`datetime.timedelta`, optional): When this handler
- is inactive more than this timeout (in seconds), it will be automatically ended. If
- this value is 0 or None (default), there will be no timeout.
+ ID. Default is :obj:`False`.
+ conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`, optional): When this
+ handler is inactive more than this timeout (in seconds), it will be automatically
+ ended. If this value is 0 or :obj:`None` (default), there will be no timeout. The last
+ received update and the corresponding ``context`` will be handled by ALL the handler's
+ who's :attr:`check_update` method returns :obj:`True` that are in the state
+ :attr:`ConversationHandler.TIMEOUT`.
+
+ Note:
+ Using `conversation_timeout` with nested conversations is currently not
+ supported. You can still try to use it, but it will likely behave differently
+ from what you expect.
+
+
+ name (:obj:`str`, optional): The name for this conversationhandler. Required for
+ persistence.
+ persistent (:obj:`bool`, optional): If the conversations dict for this handler should be
+ saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
+ map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be
+ used to instruct a nested conversationhandler to transition into a mapped state on
+ its parent conversationhandler in place of a specified nested state.
+ run_async (:obj:`bool`, optional): Pass :obj:`True` to *override* the
+ :attr:`Handler.run_async` setting of all handlers (in :attr:`entry_points`,
+ :attr:`states` and :attr:`fallbacks`).
+
+ Note:
+ If set to :obj:`True`, you should not pass a handler instance, that needs to be
+ run synchronously in another context.
+
+ .. versionadded:: 13.2
Raises:
ValueError
+ Attributes:
+ persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be
+ saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
+ run_async (:obj:`bool`): If :obj:`True`, will override the
+ :attr:`Handler.run_async` setting of all internal handlers on initialization.
+
+ .. versionadded:: 13.2
+
"""
- END = -1
- """:obj:`int`: Used as a constant to return when a conversation is ended."""
- def __init__(self,
- entry_points,
- states,
- fallbacks,
- allow_reentry=False,
- run_async_timeout=None,
- timed_out_behavior=None,
- per_chat=True,
- per_user=True,
- per_message=False,
- conversation_timeout=None):
-
- self.entry_points = entry_points
- self.states = states
- self.fallbacks = fallbacks
-
- self.allow_reentry = allow_reentry
- self.run_async_timeout = run_async_timeout
- self.timed_out_behavior = timed_out_behavior
- self.per_user = per_user
- self.per_chat = per_chat
- self.per_message = per_message
- self.conversation_timeout = conversation_timeout
-
- self.timeout_jobs = dict()
- self.conversations = dict()
- self.current_conversation = None
- self.current_handler = None
+ __slots__ = (
+ '_entry_points',
+ '_states',
+ '_fallbacks',
+ '_allow_reentry',
+ '_per_user',
+ '_per_chat',
+ '_per_message',
+ '_conversation_timeout',
+ '_name',
+ 'persistent',
+ '_persistence',
+ '_map_to_parent',
+ 'timeout_jobs',
+ '_timeout_jobs_lock',
+ '_conversations',
+ '_conversations_lock',
+ 'logger',
+ )
+
+ END: ClassVar[int] = -1
+ """:obj:`int`: Used as a constant to return when a conversation is ended."""
+ TIMEOUT: ClassVar[int] = -2
+ """:obj:`int`: Used as a constant to handle state when a conversation is timed out."""
+ WAITING: ClassVar[int] = -3
+ """:obj:`int`: Used as a constant to handle state when a conversation is still waiting on the
+ previous ``@run_sync`` decorated running handler to finish."""
+ # pylint: disable=W0231
+ def __init__(
+ self,
+ entry_points: List[Handler[Update, CCT]],
+ states: Dict[object, List[Handler[Update, CCT]]],
+ fallbacks: List[Handler[Update, CCT]],
+ allow_reentry: bool = False,
+ per_chat: bool = True,
+ per_user: bool = True,
+ per_message: bool = False,
+ conversation_timeout: Union[float, datetime.timedelta] = None,
+ name: str = None,
+ persistent: bool = False,
+ map_to_parent: Dict[object, object] = None,
+ run_async: bool = False,
+ ):
+ self.run_async = run_async
+
+ self._entry_points = entry_points
+ self._states = states
+ self._fallbacks = fallbacks
+
+ self._allow_reentry = allow_reentry
+ self._per_user = per_user
+ self._per_chat = per_chat
+ self._per_message = per_message
+ self._conversation_timeout = conversation_timeout
+ self._name = name
+ if persistent and not self.name:
+ raise ValueError("Conversations can't be persistent when handler is unnamed.")
+ self.persistent: bool = persistent
+ self._persistence: Optional[BasePersistence] = None
+ """:obj:`telegram.ext.BasePersistence`: The persistence used to store conversations.
+ Set by dispatcher"""
+ self._map_to_parent = map_to_parent
+
+ self.timeout_jobs: Dict[Tuple[int, ...], 'Job'] = {}
+ self._timeout_jobs_lock = Lock()
+ self._conversations: ConversationDict = {}
+ self._conversations_lock = Lock()
self.logger = logging.getLogger(__name__)
@@ -156,10 +260,12 @@ def __init__(self,
raise ValueError("'per_user', 'per_chat' and 'per_message' can't all be 'False'")
if self.per_message and not self.per_chat:
- logging.warning("If 'per_message=True' is used, 'per_chat=True' should also be used, "
- "since message IDs are not globally unique.")
+ warnings.warn(
+ "If 'per_message=True' is used, 'per_chat=True' should also be used, "
+ "since message IDs are not globally unique."
+ )
- all_handlers = list()
+ all_handlers: List[Handler] = []
all_handlers.extend(entry_points)
all_handlers.extend(fallbacks)
@@ -169,165 +275,451 @@ def __init__(self,
if self.per_message:
for handler in all_handlers:
if not isinstance(handler, CallbackQueryHandler):
- logging.warning("If 'per_message=True', all entry points and state handlers"
- " must be 'CallbackQueryHandler', since no other handlers "
- "have a message context.")
+ warnings.warn(
+ "If 'per_message=True', all entry points and state handlers"
+ " must be 'CallbackQueryHandler', since no other handlers "
+ "have a message context."
+ )
+ break
else:
for handler in all_handlers:
if isinstance(handler, CallbackQueryHandler):
- logging.warning("If 'per_message=False', 'CallbackQueryHandler' will not be "
- "tracked for every message.")
+ warnings.warn(
+ "If 'per_message=False', 'CallbackQueryHandler' will not be "
+ "tracked for every message."
+ )
+ break
if self.per_chat:
for handler in all_handlers:
if isinstance(handler, (InlineQueryHandler, ChosenInlineResultHandler)):
- logging.warning("If 'per_chat=True', 'InlineQueryHandler' can not be used, "
- "since inline queries have no chat context.")
+ warnings.warn(
+ "If 'per_chat=True', 'InlineQueryHandler' can not be used, "
+ "since inline queries have no chat context."
+ )
+ break
+
+ if self.conversation_timeout:
+ for handler in all_handlers:
+ if isinstance(handler, self.__class__):
+ warnings.warn(
+ "Using `conversation_timeout` with nested conversations is currently not "
+ "supported. You can still try to use it, but it will likely behave "
+ "differently from what you expect."
+ )
+ break
+
+ if self.run_async:
+ for handler in all_handlers:
+ handler.run_async = True
+
+ @property
+ def entry_points(self) -> List[Handler]:
+ """List[:class:`telegram.ext.Handler`]: A list of ``Handler`` objects that can trigger the
+ start of the conversation.
+ """
+ return self._entry_points
+
+ @entry_points.setter
+ def entry_points(self, value: object) -> NoReturn:
+ raise ValueError('You can not assign a new value to entry_points after initialization.')
+
+ @property
+ def states(self) -> Dict[object, List[Handler]]:
+ """Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]: A :obj:`dict` that
+ defines the different states of conversation a user can be in and one or more
+ associated ``Handler`` objects that should be used in that state.
+ """
+ return self._states
- def _get_key(self, update):
+ @states.setter
+ def states(self, value: object) -> NoReturn:
+ raise ValueError('You can not assign a new value to states after initialization.')
+
+ @property
+ def fallbacks(self) -> List[Handler]:
+ """List[:class:`telegram.ext.Handler`]: A list of handlers that might be used if
+ the user is in a conversation, but every handler for their current state returned
+ :obj:`False` on :attr:`check_update`.
+ """
+ return self._fallbacks
+
+ @fallbacks.setter
+ def fallbacks(self, value: object) -> NoReturn:
+ raise ValueError('You can not assign a new value to fallbacks after initialization.')
+
+ @property
+ def allow_reentry(self) -> bool:
+ """:obj:`bool`: Determines if a user can restart a conversation with an entry point."""
+ return self._allow_reentry
+
+ @allow_reentry.setter
+ def allow_reentry(self, value: object) -> NoReturn:
+ raise ValueError('You can not assign a new value to allow_reentry after initialization.')
+
+ @property
+ def per_user(self) -> bool:
+ """:obj:`bool`: If the conversation key should contain the User's ID."""
+ return self._per_user
+
+ @per_user.setter
+ def per_user(self, value: object) -> NoReturn:
+ raise ValueError('You can not assign a new value to per_user after initialization.')
+
+ @property
+ def per_chat(self) -> bool:
+ """:obj:`bool`: If the conversation key should contain the Chat's ID."""
+ return self._per_chat
+
+ @per_chat.setter
+ def per_chat(self, value: object) -> NoReturn:
+ raise ValueError('You can not assign a new value to per_chat after initialization.')
+
+ @property
+ def per_message(self) -> bool:
+ """:obj:`bool`: If the conversation key should contain the message's ID."""
+ return self._per_message
+
+ @per_message.setter
+ def per_message(self, value: object) -> NoReturn:
+ raise ValueError('You can not assign a new value to per_message after initialization.')
+
+ @property
+ def conversation_timeout(
+ self,
+ ) -> Optional[Union[float, datetime.timedelta]]:
+ """:obj:`float` | :obj:`datetime.timedelta`: Optional. When this
+ handler is inactive more than this timeout (in seconds), it will be automatically
+ ended.
+ """
+ return self._conversation_timeout
+
+ @conversation_timeout.setter
+ def conversation_timeout(self, value: object) -> NoReturn:
+ raise ValueError(
+ 'You can not assign a new value to conversation_timeout after initialization.'
+ )
+
+ @property
+ def name(self) -> Optional[str]:
+ """:obj:`str`: Optional. The name for this :class:`ConversationHandler`."""
+ return self._name
+
+ @name.setter
+ def name(self, value: object) -> NoReturn:
+ raise ValueError('You can not assign a new value to name after initialization.')
+
+ @property
+ def map_to_parent(self) -> Optional[Dict[object, object]]:
+ """Dict[:obj:`object`, :obj:`object`]: Optional. A :obj:`dict` that can be
+ used to instruct a nested :class:`ConversationHandler` to transition into a mapped state on
+ its parent :class:`ConversationHandler` in place of a specified nested state.
+ """
+ return self._map_to_parent
+
+ @map_to_parent.setter
+ def map_to_parent(self, value: object) -> NoReturn:
+ raise ValueError('You can not assign a new value to map_to_parent after initialization.')
+
+ @property
+ def persistence(self) -> Optional[BasePersistence]:
+ """The persistence class as provided by the :class:`Dispatcher`."""
+ return self._persistence
+
+ @persistence.setter
+ def persistence(self, persistence: BasePersistence) -> None:
+ self._persistence = persistence
+ # Set persistence for nested conversations
+ for handlers in self.states.values():
+ for handler in handlers:
+ if isinstance(handler, ConversationHandler):
+ handler.persistence = self.persistence
+
+ @property
+ def conversations(self) -> ConversationDict: # skipcq: PY-D0003
+ return self._conversations
+
+ @conversations.setter
+ def conversations(self, value: ConversationDict) -> None:
+ self._conversations = value
+ # Set conversations for nested conversations
+ for handlers in self.states.values():
+ for handler in handlers:
+ if isinstance(handler, ConversationHandler) and self.persistence and handler.name:
+ handler.conversations = self.persistence.get_conversations(handler.name)
+
+ def _get_key(self, update: Update) -> Tuple[int, ...]:
chat = update.effective_chat
user = update.effective_user
- key = list()
+ key = []
if self.per_chat:
- key.append(chat.id)
+ key.append(chat.id) # type: ignore[union-attr]
if self.per_user and user is not None:
key.append(user.id)
if self.per_message:
- key.append(update.callback_query.inline_message_id
- or update.callback_query.message.message_id)
+ key.append(
+ update.callback_query.inline_message_id # type: ignore[union-attr]
+ or update.callback_query.message.message_id # type: ignore[union-attr]
+ )
return tuple(key)
- def check_update(self, update):
+ def _resolve_promise(self, state: Tuple) -> object:
+ old_state, new_state = state
+ try:
+ res = new_state.result(0)
+ res = res if res is not None else old_state
+ except Exception as exc:
+ self.logger.exception("Promise function raised exception")
+ self.logger.exception("%s", exc)
+ res = old_state
+ finally:
+ if res is None and old_state is None:
+ res = self.END
+ return res
+
+ def _schedule_job(
+ self,
+ new_state: object,
+ dispatcher: 'Dispatcher',
+ update: Update,
+ context: Optional[CallbackContext],
+ conversation_key: Tuple[int, ...],
+ ) -> None:
+ if new_state != self.END:
+ try:
+ # both job_queue & conversation_timeout are checked before calling _schedule_job
+ j_queue = dispatcher.job_queue
+ self.timeout_jobs[conversation_key] = j_queue.run_once( # type: ignore[union-attr]
+ self._trigger_timeout,
+ self.conversation_timeout, # type: ignore[arg-type]
+ context=_ConversationTimeoutContext(
+ conversation_key, update, dispatcher, context
+ ),
+ )
+ except Exception as exc:
+ self.logger.exception(
+ "Failed to schedule timeout job due to the following exception:"
+ )
+ self.logger.exception("%s", exc)
+
+ def check_update(self, update: object) -> CheckUpdateType: # pylint: disable=R0911
"""
Determines whether an update should be handled by this conversationhandler, and if so in
which state the conversation currently is.
Args:
- update (:class:`telegram.Update`): Incoming telegram update.
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
+ if not isinstance(update, Update):
+ return None
# Ignore messages in channels
- if (not isinstance(update, Update) or
- update.channel_post or
- self.per_chat and not update.effective_chat or
- self.per_message and not update.callback_query or
- update.callback_query and self.per_chat and not update.callback_query.message):
- return False
+ if update.channel_post or update.edited_channel_post:
+ return None
+ if self.per_chat and not update.effective_chat:
+ return None
+ if self.per_message and not update.callback_query:
+ return None
+ if update.callback_query and self.per_chat and not update.callback_query.message:
+ return None
key = self._get_key(update)
- state = self.conversations.get(key)
+ with self._conversations_lock:
+ state = self.conversations.get(key)
# Resolve promises
- if isinstance(state, tuple) and len(state) is 2 and isinstance(state[1], Promise):
+ if isinstance(state, tuple) and len(state) == 2 and isinstance(state[1], Promise):
self.logger.debug('waiting for promise...')
- old_state, new_state = state
- error = False
- try:
- res = new_state.result(timeout=self.run_async_timeout)
- except Exception as exc:
- self.logger.exception("Promise function raised exception")
- self.logger.exception("{}".format(exc))
- error = True
-
- if not error and new_state.done.is_set():
- self.update_state(res, key)
- state = self.conversations.get(key)
+ # check if promise is finished or not
+ if state[1].done.wait(0):
+ res = self._resolve_promise(state)
+ self._update_state(res, key)
+ with self._conversations_lock:
+ state = self.conversations.get(key)
+ # if not then handle WAITING state instead
else:
- for candidate in (self.timed_out_behavior or []):
- if candidate.check_update(update):
- # Save the current user and the selected handler for handle_update
- self.current_conversation = key
- self.current_handler = candidate
+ hdlrs = self.states.get(self.WAITING, [])
+ for hdlr in hdlrs:
+ check = hdlr.check_update(update)
+ if check is not None and check is not False:
+ return key, hdlr, check
+ return None
- return True
-
- else:
- return False
-
- self.logger.debug('selecting conversation %s with state %s' % (str(key), str(state)))
+ self.logger.debug('selecting conversation %s with state %s', str(key), str(state))
handler = None
# Search entry points for a match
if state is None or self.allow_reentry:
for entry_point in self.entry_points:
- if entry_point.check_update(update):
+ check = entry_point.check_update(update)
+ if check is not None and check is not False:
handler = entry_point
break
else:
if state is None:
- return False
+ return None
# Get the handler list for current state, if we didn't find one yet and we're still here
if state is not None and not handler:
handlers = self.states.get(state)
- for candidate in (handlers or []):
- if candidate.check_update(update):
+ for candidate in handlers or []:
+ check = candidate.check_update(update)
+ if check is not None and check is not False:
handler = candidate
break
# Find a fallback handler if all other handlers fail
else:
for fallback in self.fallbacks:
- if fallback.check_update(update):
+ check = fallback.check_update(update)
+ if check is not None and check is not False:
handler = fallback
break
else:
- return False
-
- # Save the current user and the selected handler for handle_update
- self.current_conversation = key
- self.current_handler = handler
+ return None
- return True
+ return key, handler, check # type: ignore[return-value]
- def handle_update(self, update, dispatcher):
+ def handle_update( # type: ignore[override]
+ self,
+ update: Update,
+ dispatcher: 'Dispatcher',
+ check_result: CheckUpdateType,
+ context: CallbackContext = None,
+ ) -> Optional[object]:
"""Send the update to the callback for the current state and Handler
Args:
+ check_result: The result from check_update. For this handler it's a tuple of key,
+ handler, and the handler's check result.
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
+ context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by
+ the dispatcher.
"""
- new_state = self.current_handler.handle_update(update, dispatcher)
- timeout_job = self.timeout_jobs.pop(self.current_conversation, None)
-
- if timeout_job is not None:
- timeout_job.schedule_removal()
- if self.conversation_timeout and new_state != self.END:
- self.timeout_jobs[self.current_conversation] = dispatcher.job_queue.run_once(
- self._trigger_timeout, self.conversation_timeout,
- context=self.current_conversation
- )
-
- self.update_state(new_state, self.current_conversation)
-
- def update_state(self, new_state, key):
+ update = cast(Update, update) # for mypy
+ conversation_key, handler, check_result = check_result # type: ignore[assignment,misc]
+ raise_dp_handler_stop = False
+
+ with self._timeout_jobs_lock:
+ # Remove the old timeout job (if present)
+ timeout_job = self.timeout_jobs.pop(conversation_key, None)
+
+ if timeout_job is not None:
+ timeout_job.schedule_removal()
+ try:
+ new_state = handler.handle_update(update, dispatcher, check_result, context)
+ except DispatcherHandlerStop as exception:
+ new_state = exception.state
+ raise_dp_handler_stop = True
+ with self._timeout_jobs_lock:
+ if self.conversation_timeout:
+ if dispatcher.job_queue is not None:
+ # Add the new timeout job
+ if isinstance(new_state, Promise):
+ new_state.add_done_callback(
+ functools.partial(
+ self._schedule_job,
+ dispatcher=dispatcher,
+ update=update,
+ context=context,
+ conversation_key=conversation_key,
+ )
+ )
+ elif new_state != self.END:
+ self._schedule_job(
+ new_state, dispatcher, update, context, conversation_key
+ )
+ else:
+ self.logger.warning(
+ "Ignoring `conversation_timeout` because the Dispatcher has no JobQueue."
+ )
+
+ if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
+ self._update_state(self.END, conversation_key)
+ if raise_dp_handler_stop:
+ raise DispatcherHandlerStop(self.map_to_parent.get(new_state))
+ return self.map_to_parent.get(new_state)
+
+ self._update_state(new_state, conversation_key)
+ if raise_dp_handler_stop:
+ # Don't pass the new state here. If we're in a nested conversation, the parent is
+ # expecting None as return value.
+ raise DispatcherHandlerStop()
+ return None
+
+ def _update_state(self, new_state: object, key: Tuple[int, ...]) -> None:
if new_state == self.END:
- if key in self.conversations:
- del self.conversations[key]
- else:
- pass
+ with self._conversations_lock:
+ if key in self.conversations:
+ # If there is no key in conversations, nothing is done.
+ del self.conversations[key]
+ if self.persistent and self.persistence and self.name:
+ self.persistence.update_conversation(self.name, key, None)
elif isinstance(new_state, Promise):
- self.conversations[key] = (self.conversations.get(key), new_state)
+ with self._conversations_lock:
+ self.conversations[key] = (self.conversations.get(key), new_state)
+ if self.persistent and self.persistence and self.name:
+ self.persistence.update_conversation(
+ self.name, key, (self.conversations.get(key), new_state)
+ )
elif new_state is not None:
- self.conversations[key] = new_state
-
- def _trigger_timeout(self, bot, job):
- del self.timeout_jobs[job.context]
- self.update_state(self.END, job.context)
+ if new_state not in self.states:
+ warnings.warn(
+ f"Handler returned state {new_state} which is unknown to the "
+ f"ConversationHandler{' ' + self.name if self.name is not None else ''}."
+ )
+ with self._conversations_lock:
+ self.conversations[key] = new_state
+ if self.persistent and self.persistence and self.name:
+ self.persistence.update_conversation(self.name, key, new_state)
+
+ def _trigger_timeout(self, context: CallbackContext, job: 'Job' = None) -> None:
+ self.logger.debug('conversation timeout was triggered!')
+
+ # Backward compatibility with bots that do not use CallbackContext
+ if isinstance(context, CallbackContext):
+ job = context.job
+ ctxt = cast(_ConversationTimeoutContext, job.context) # type: ignore[union-attr]
+ else:
+ ctxt = cast(_ConversationTimeoutContext, job.context)
+
+ callback_context = ctxt.callback_context
+
+ with self._timeout_jobs_lock:
+ found_job = self.timeout_jobs[ctxt.conversation_key]
+ if found_job is not job:
+ # The timeout has been cancelled in handle_update
+ return
+ del self.timeout_jobs[ctxt.conversation_key]
+
+ handlers = self.states.get(self.TIMEOUT, [])
+ for handler in handlers:
+ check = handler.check_update(ctxt.update)
+ if check is not None and check is not False:
+ try:
+ handler.handle_update(ctxt.update, ctxt.dispatcher, check, callback_context)
+ except DispatcherHandlerStop:
+ self.logger.warning(
+ 'DispatcherHandlerStop in TIMEOUT state of '
+ 'ConversationHandler has no effect. Ignoring.'
+ )
+
+ self._update_state(self.END, ctxt.conversation_key)
diff --git a/telegramer/include/telegram/ext/defaults.py b/telegramer/include/telegram/ext/defaults.py
new file mode 100644
index 0000000..1e08255
--- /dev/null
+++ b/telegramer/include/telegram/ext/defaults.py
@@ -0,0 +1,267 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=R0201
+"""This module contains the class Defaults, which allows to pass default values to Updater."""
+from typing import NoReturn, Optional, Dict, Any
+
+import pytz
+
+from telegram.utils.deprecate import set_new_attribute_deprecated
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+
+class Defaults:
+ """Convenience Class to gather all parameters with a (user defined) default value
+
+ Parameters:
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ bold, italic, fixed-width text or URLs in your bot's message.
+ disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
+ receive a notification with no sound.
+ disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this
+ message.
+ allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the
+ read timeout from the server (instead of the one specified during creation of the
+ connection pool).
+
+ Note:
+ Will *not* be used for :meth:`telegram.Bot.get_updates`!
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply
+ to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
+ be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
+ tzinfo (:obj:`tzinfo`, optional): A timezone to be used for all date(time) inputs
+ appearing throughout PTB, i.e. if a timezone naive date(time) object is passed
+ somewhere, it will be assumed to be in ``tzinfo``. Must be a timezone provided by the
+ ``pytz`` module. Defaults to UTC.
+ run_async (:obj:`bool`, optional): Default setting for the ``run_async`` parameter of
+ handlers and error handlers registered through :meth:`Dispatcher.add_handler` and
+ :meth:`Dispatcher.add_error_handler`. Defaults to :obj:`False`.
+ """
+
+ __slots__ = (
+ '_timeout',
+ '_tzinfo',
+ '_disable_web_page_preview',
+ '_run_async',
+ '_quote',
+ '_disable_notification',
+ '_allow_sending_without_reply',
+ '_parse_mode',
+ '_api_defaults',
+ '__dict__',
+ )
+
+ def __init__(
+ self,
+ parse_mode: str = None,
+ disable_notification: bool = None,
+ disable_web_page_preview: bool = None,
+ # Timeout needs special treatment, since the bot methods have two different
+ # default values for timeout (None and 20s)
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ quote: bool = None,
+ tzinfo: pytz.BaseTzInfo = pytz.utc,
+ run_async: bool = False,
+ allow_sending_without_reply: bool = None,
+ ):
+ self._parse_mode = parse_mode
+ self._disable_notification = disable_notification
+ self._disable_web_page_preview = disable_web_page_preview
+ self._allow_sending_without_reply = allow_sending_without_reply
+ self._timeout = timeout
+ self._quote = quote
+ self._tzinfo = tzinfo
+ self._run_async = run_async
+
+ # Gather all defaults that actually have a default value
+ self._api_defaults = {}
+ for kwarg in (
+ 'parse_mode',
+ 'explanation_parse_mode',
+ 'disable_notification',
+ 'disable_web_page_preview',
+ 'allow_sending_without_reply',
+ ):
+ value = getattr(self, kwarg)
+ if value not in [None, DEFAULT_NONE]:
+ self._api_defaults[kwarg] = value
+ # Special casing, as None is a valid default value
+ if self._timeout != DEFAULT_NONE:
+ self._api_defaults['timeout'] = self._timeout
+
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
+
+ @property
+ def api_defaults(self) -> Dict[str, Any]: # skip-cq: PY-D0003
+ return self._api_defaults
+
+ @property
+ def parse_mode(self) -> Optional[str]:
+ """:obj:`str`: Optional. Send Markdown or HTML, if you want Telegram apps to show
+ bold, italic, fixed-width text or URLs in your bot's message.
+ """
+ return self._parse_mode
+
+ @parse_mode.setter
+ def parse_mode(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to defaults after because it would "
+ "not have any effect."
+ )
+
+ @property
+ def explanation_parse_mode(self) -> Optional[str]:
+ """:obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
+ the corresponding parameter of :meth:`telegram.Bot.send_poll`.
+ """
+ return self._parse_mode
+
+ @explanation_parse_mode.setter
+ def explanation_parse_mode(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to defaults after because it would "
+ "not have any effect."
+ )
+
+ @property
+ def disable_notification(self) -> Optional[bool]:
+ """:obj:`bool`: Optional. Sends the message silently. Users will
+ receive a notification with no sound.
+ """
+ return self._disable_notification
+
+ @disable_notification.setter
+ def disable_notification(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to defaults after because it would "
+ "not have any effect."
+ )
+
+ @property
+ def disable_web_page_preview(self) -> Optional[bool]:
+ """:obj:`bool`: Optional. Disables link previews for links in this
+ message.
+ """
+ return self._disable_web_page_preview
+
+ @disable_web_page_preview.setter
+ def disable_web_page_preview(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to defaults after because it would "
+ "not have any effect."
+ )
+
+ @property
+ def allow_sending_without_reply(self) -> Optional[bool]:
+ """:obj:`bool`: Optional. Pass :obj:`True`, if the message
+ should be sent even if the specified replied-to message is not found.
+ """
+ return self._allow_sending_without_reply
+
+ @allow_sending_without_reply.setter
+ def allow_sending_without_reply(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to defaults after because it would "
+ "not have any effect."
+ )
+
+ @property
+ def timeout(self) -> ODVInput[float]:
+ """:obj:`int` | :obj:`float`: Optional. If this value is specified, use it as the
+ read timeout from the server (instead of the one specified during creation of the
+ connection pool).
+ """
+ return self._timeout
+
+ @timeout.setter
+ def timeout(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to defaults after because it would "
+ "not have any effect."
+ )
+
+ @property
+ def quote(self) -> Optional[bool]:
+ """:obj:`bool`: Optional. If set to :obj:`True`, the reply is sent as an actual reply
+ to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
+ be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
+ """
+ return self._quote
+
+ @quote.setter
+ def quote(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to defaults after because it would "
+ "not have any effect."
+ )
+
+ @property
+ def tzinfo(self) -> pytz.BaseTzInfo:
+ """:obj:`tzinfo`: A timezone to be used for all date(time) objects appearing
+ throughout PTB.
+ """
+ return self._tzinfo
+
+ @tzinfo.setter
+ def tzinfo(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to defaults after because it would "
+ "not have any effect."
+ )
+
+ @property
+ def run_async(self) -> bool:
+ """:obj:`bool`: Optional. Default setting for the ``run_async`` parameter of
+ handlers and error handlers registered through :meth:`Dispatcher.add_handler` and
+ :meth:`Dispatcher.add_error_handler`.
+ """
+ return self._run_async
+
+ @run_async.setter
+ def run_async(self, value: object) -> NoReturn:
+ raise AttributeError(
+ "You can not assign a new value to defaults after because it would "
+ "not have any effect."
+ )
+
+ def __hash__(self) -> int:
+ return hash(
+ (
+ self._parse_mode,
+ self._disable_notification,
+ self._disable_web_page_preview,
+ self._allow_sending_without_reply,
+ self._timeout,
+ self._quote,
+ self._tzinfo,
+ self._run_async,
+ )
+ )
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, Defaults):
+ return all(getattr(self, attr) == getattr(other, attr) for attr in self.__slots__)
+ return False
+
+ def __ne__(self, other: object) -> bool:
+ return not self == other
diff --git a/telegramer/include/telegram/ext/dictpersistence.py b/telegramer/include/telegram/ext/dictpersistence.py
new file mode 100644
index 0000000..e307123
--- /dev/null
+++ b/telegramer/include/telegram/ext/dictpersistence.py
@@ -0,0 +1,404 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains the DictPersistence class."""
+
+from typing import DefaultDict, Dict, Optional, Tuple, cast
+from collections import defaultdict
+
+from telegram.utils.helpers import (
+ decode_conversations_from_json,
+ decode_user_chat_data_from_json,
+ encode_conversations_to_json,
+)
+from telegram.ext import BasePersistence
+from telegram.ext.utils.types import ConversationDict, CDCData
+
+try:
+ import ujson as json
+except ImportError:
+ import json # type: ignore[no-redef]
+
+
+class DictPersistence(BasePersistence):
+ """Using Python's :obj:`dict` and ``json`` for making your bot persistent.
+
+ Note:
+ This class does *not* implement a :meth:`flush` method, meaning that data managed by
+ ``DictPersistence`` is in-memory only and will be lost when the bot shuts down. This is,
+ because ``DictPersistence`` is mainly intended as starting point for custom persistence
+ classes that need to JSON-serialize the stored data before writing them to file/database.
+
+ Warning:
+ :class:`DictPersistence` will try to replace :class:`telegram.Bot` instances by
+ :attr:`REPLACED_BOT` and insert the bot set with
+ :meth:`telegram.ext.BasePersistence.set_bot` upon loading of the data. This is to ensure
+ that changes to the bot apply to the saved objects, too. If you change the bots token, this
+ may lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see
+ :meth:`telegram.ext.BasePersistence.replace_bot` and
+ :meth:`telegram.ext.BasePersistence.insert_bot`.
+
+ Args:
+ store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
+ persistence class. Default is :obj:`True`.
+ store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this
+ persistence class. Default is :obj:`True`.
+ store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this
+ persistence class. Default is :obj:`True`.
+ store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this
+ persistence class. Default is :obj:`False`.
+
+ .. versionadded:: 13.6
+ user_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct
+ user_data on creating this persistence. Default is ``""``.
+ chat_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct
+ chat_data on creating this persistence. Default is ``""``.
+ bot_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct
+ bot_data on creating this persistence. Default is ``""``.
+ callback_data_json (:obj:`str`, optional): Json string that will be used to reconstruct
+ callback_data on creating this persistence. Default is ``""``.
+
+ .. versionadded:: 13.6
+ conversations_json (:obj:`str`, optional): JSON string that will be used to reconstruct
+ conversation on creating this persistence. Default is ``""``.
+
+ Attributes:
+ store_user_data (:obj:`bool`): Whether user_data should be saved by this
+ persistence class.
+ store_chat_data (:obj:`bool`): Whether chat_data should be saved by this
+ persistence class.
+ store_bot_data (:obj:`bool`): Whether bot_data should be saved by this
+ persistence class.
+ store_callback_data (:obj:`bool`): Whether callback_data be saved by this
+ persistence class.
+
+ .. versionadded:: 13.6
+ """
+
+ __slots__ = (
+ '_user_data',
+ '_chat_data',
+ '_bot_data',
+ '_callback_data',
+ '_conversations',
+ '_user_data_json',
+ '_chat_data_json',
+ '_bot_data_json',
+ '_callback_data_json',
+ '_conversations_json',
+ )
+
+ def __init__(
+ self,
+ store_user_data: bool = True,
+ store_chat_data: bool = True,
+ store_bot_data: bool = True,
+ user_data_json: str = '',
+ chat_data_json: str = '',
+ bot_data_json: str = '',
+ conversations_json: str = '',
+ store_callback_data: bool = False,
+ callback_data_json: str = '',
+ ):
+ super().__init__(
+ store_user_data=store_user_data,
+ store_chat_data=store_chat_data,
+ store_bot_data=store_bot_data,
+ store_callback_data=store_callback_data,
+ )
+ self._user_data = None
+ self._chat_data = None
+ self._bot_data = None
+ self._callback_data = None
+ self._conversations = None
+ self._user_data_json = None
+ self._chat_data_json = None
+ self._bot_data_json = None
+ self._callback_data_json = None
+ self._conversations_json = None
+ if user_data_json:
+ try:
+ self._user_data = decode_user_chat_data_from_json(user_data_json)
+ self._user_data_json = user_data_json
+ except (ValueError, AttributeError) as exc:
+ raise TypeError("Unable to deserialize user_data_json. Not valid JSON") from exc
+ if chat_data_json:
+ try:
+ self._chat_data = decode_user_chat_data_from_json(chat_data_json)
+ self._chat_data_json = chat_data_json
+ except (ValueError, AttributeError) as exc:
+ raise TypeError("Unable to deserialize chat_data_json. Not valid JSON") from exc
+ if bot_data_json:
+ try:
+ self._bot_data = json.loads(bot_data_json)
+ self._bot_data_json = bot_data_json
+ except (ValueError, AttributeError) as exc:
+ raise TypeError("Unable to deserialize bot_data_json. Not valid JSON") from exc
+ if not isinstance(self._bot_data, dict):
+ raise TypeError("bot_data_json must be serialized dict")
+ if callback_data_json:
+ try:
+ data = json.loads(callback_data_json)
+ except (ValueError, AttributeError) as exc:
+ raise TypeError(
+ "Unable to deserialize callback_data_json. Not valid JSON"
+ ) from exc
+ # We are a bit more thorough with the checking of the format here, because it's
+ # more complicated than for the other things
+ try:
+ if data is None:
+ self._callback_data = None
+ else:
+ self._callback_data = cast(
+ CDCData,
+ ([(one, float(two), three) for one, two, three in data[0]], data[1]),
+ )
+ self._callback_data_json = callback_data_json
+ except (ValueError, IndexError) as exc:
+ raise TypeError("callback_data_json is not in the required format") from exc
+ if self._callback_data is not None and (
+ not all(
+ isinstance(entry[2], dict) and isinstance(entry[0], str)
+ for entry in self._callback_data[0]
+ )
+ or not isinstance(self._callback_data[1], dict)
+ ):
+ raise TypeError("callback_data_json is not in the required format")
+
+ if conversations_json:
+ try:
+ self._conversations = decode_conversations_from_json(conversations_json)
+ self._conversations_json = conversations_json
+ except (ValueError, AttributeError) as exc:
+ raise TypeError(
+ "Unable to deserialize conversations_json. Not valid JSON"
+ ) from exc
+
+ @property
+ def user_data(self) -> Optional[DefaultDict[int, Dict]]:
+ """:obj:`dict`: The user_data as a dict."""
+ return self._user_data
+
+ @property
+ def user_data_json(self) -> str:
+ """:obj:`str`: The user_data serialized as a JSON-string."""
+ if self._user_data_json:
+ return self._user_data_json
+ return json.dumps(self.user_data)
+
+ @property
+ def chat_data(self) -> Optional[DefaultDict[int, Dict]]:
+ """:obj:`dict`: The chat_data as a dict."""
+ return self._chat_data
+
+ @property
+ def chat_data_json(self) -> str:
+ """:obj:`str`: The chat_data serialized as a JSON-string."""
+ if self._chat_data_json:
+ return self._chat_data_json
+ return json.dumps(self.chat_data)
+
+ @property
+ def bot_data(self) -> Optional[Dict]:
+ """:obj:`dict`: The bot_data as a dict."""
+ return self._bot_data
+
+ @property
+ def bot_data_json(self) -> str:
+ """:obj:`str`: The bot_data serialized as a JSON-string."""
+ if self._bot_data_json:
+ return self._bot_data_json
+ return json.dumps(self.bot_data)
+
+ @property
+ def callback_data(self) -> Optional[CDCData]:
+ """:class:`telegram.ext.utils.types.CDCData`: The meta data on the stored callback data.
+
+ .. versionadded:: 13.6
+ """
+ return self._callback_data
+
+ @property
+ def callback_data_json(self) -> str:
+ """:obj:`str`: The meta data on the stored callback data as a JSON-string.
+
+ .. versionadded:: 13.6
+ """
+ if self._callback_data_json:
+ return self._callback_data_json
+ return json.dumps(self.callback_data)
+
+ @property
+ def conversations(self) -> Optional[Dict[str, ConversationDict]]:
+ """:obj:`dict`: The conversations as a dict."""
+ return self._conversations
+
+ @property
+ def conversations_json(self) -> str:
+ """:obj:`str`: The conversations serialized as a JSON-string."""
+ if self._conversations_json:
+ return self._conversations_json
+ return encode_conversations_to_json(self.conversations) # type: ignore[arg-type]
+
+ def get_user_data(self) -> DefaultDict[int, Dict[object, object]]:
+ """Returns the user_data created from the ``user_data_json`` or an empty
+ :obj:`defaultdict`.
+
+ Returns:
+ :obj:`defaultdict`: The restored user data.
+ """
+ if self.user_data is None:
+ self._user_data = defaultdict(dict)
+ return self.user_data # type: ignore[return-value]
+
+ def get_chat_data(self) -> DefaultDict[int, Dict[object, object]]:
+ """Returns the chat_data created from the ``chat_data_json`` or an empty
+ :obj:`defaultdict`.
+
+ Returns:
+ :obj:`defaultdict`: The restored chat data.
+ """
+ if self.chat_data is None:
+ self._chat_data = defaultdict(dict)
+ return self.chat_data # type: ignore[return-value]
+
+ def get_bot_data(self) -> Dict[object, object]:
+ """Returns the bot_data created from the ``bot_data_json`` or an empty :obj:`dict`.
+
+ Returns:
+ :obj:`dict`: The restored bot data.
+ """
+ if self.bot_data is None:
+ self._bot_data = {}
+ return self.bot_data # type: ignore[return-value]
+
+ def get_callback_data(self) -> Optional[CDCData]:
+ """Returns the callback_data created from the ``callback_data_json`` or :obj:`None`.
+
+ .. versionadded:: 13.6
+
+ Returns:
+ Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or
+ :obj:`None`, if no data was stored.
+ """
+ if self.callback_data is None:
+ self._callback_data = None
+ return None
+ return self.callback_data[0], self.callback_data[1].copy()
+
+ def get_conversations(self, name: str) -> ConversationDict:
+ """Returns the conversations created from the ``conversations_json`` or an empty
+ :obj:`dict`.
+
+ Returns:
+ :obj:`dict`: The restored conversations data.
+ """
+ if self.conversations is None:
+ self._conversations = {}
+ return self.conversations.get(name, {}).copy() # type: ignore[union-attr]
+
+ def update_conversation(
+ self, name: str, key: Tuple[int, ...], new_state: Optional[object]
+ ) -> None:
+ """Will update the conversations for the given handler.
+
+ Args:
+ name (:obj:`str`): The handler's name.
+ key (:obj:`tuple`): The key the state is changed for.
+ new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
+ """
+ if not self._conversations:
+ self._conversations = {}
+ if self._conversations.setdefault(name, {}).get(key) == new_state:
+ return
+ self._conversations[name][key] = new_state
+ self._conversations_json = None
+
+ def update_user_data(self, user_id: int, data: Dict) -> None:
+ """Will update the user_data (if changed).
+
+ Args:
+ user_id (:obj:`int`): The user the data might have been changed for.
+ data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``.
+ """
+ if self._user_data is None:
+ self._user_data = defaultdict(dict)
+ if self._user_data.get(user_id) == data:
+ return
+ self._user_data[user_id] = data
+ self._user_data_json = None
+
+ def update_chat_data(self, chat_id: int, data: Dict) -> None:
+ """Will update the chat_data (if changed).
+
+ Args:
+ chat_id (:obj:`int`): The chat the data might have been changed for.
+ data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``.
+ """
+ if self._chat_data is None:
+ self._chat_data = defaultdict(dict)
+ if self._chat_data.get(chat_id) == data:
+ return
+ self._chat_data[chat_id] = data
+ self._chat_data_json = None
+
+ def update_bot_data(self, data: Dict) -> None:
+ """Will update the bot_data (if changed).
+
+ Args:
+ data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.bot_data`.
+ """
+ if self._bot_data == data:
+ return
+ self._bot_data = data
+ self._bot_data_json = None
+
+ def update_callback_data(self, data: CDCData) -> None:
+ """Will update the callback_data (if changed).
+
+ .. versionadded:: 13.6
+
+ Args:
+ data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore
+ :class:`telegram.ext.CallbackDataCache`.
+ """
+ if self._callback_data == data:
+ return
+ self._callback_data = (data[0], data[1].copy())
+ self._callback_data_json = None
+
+ def refresh_user_data(self, user_id: int, user_data: Dict) -> None:
+ """Does nothing.
+
+ .. versionadded:: 13.6
+ .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data`
+ """
+
+ def refresh_chat_data(self, chat_id: int, chat_data: Dict) -> None:
+ """Does nothing.
+
+ .. versionadded:: 13.6
+ .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data`
+ """
+
+ def refresh_bot_data(self, bot_data: Dict) -> None:
+ """Does nothing.
+
+ .. versionadded:: 13.6
+ .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data`
+ """
diff --git a/telegramer/include/telegram/ext/dispatcher.py b/telegramer/include/telegram/ext/dispatcher.py
index 0f43fdb..af24188 100644
--- a/telegramer/include/telegram/ext/dispatcher.py
+++ b/telegramer/include/telegram/ext/dispatcher.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,58 +19,112 @@
"""This module contains the Dispatcher class."""
import logging
+import warnings
import weakref
+from collections import defaultdict
from functools import wraps
-from threading import Thread, Lock, Event, current_thread, BoundedSemaphore
+from queue import Empty, Queue
+from threading import BoundedSemaphore, Event, Lock, Thread, current_thread
from time import sleep
+from typing import (
+ TYPE_CHECKING,
+ Callable,
+ DefaultDict,
+ Dict,
+ List,
+ Optional,
+ Set,
+ Union,
+ Generic,
+ TypeVar,
+ overload,
+ cast,
+)
from uuid import uuid4
-from collections import defaultdict
-# REMREM from queue import Queue, Empty
-from Queue import Queue, Empty
+from telegram import TelegramError, Update
+from telegram.ext import BasePersistence, ContextTypes
+from telegram.ext.callbackcontext import CallbackContext
+from telegram.ext.handler import Handler
+import telegram.ext.extbot
+from telegram.ext.callbackdatacache import CallbackDataCache
+from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated
+from telegram.ext.utils.promise import Promise
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+from telegram.ext.utils.types import CCT, UD, CD, BD
-from future.builtins import range
+if TYPE_CHECKING:
+ from telegram import Bot
+ from telegram.ext import JobQueue
-from telegram import TelegramError
-from telegram.ext.handler import Handler
-from telegram.utils.promise import Promise
+DEFAULT_GROUP: int = 0
-logging.getLogger(__name__).addHandler(logging.NullHandler())
-DEFAULT_GROUP = 0
+UT = TypeVar('UT')
-def run_async(func):
- """Function decorator that will run the function in a new thread.
+def run_async(
+ func: Callable[[Update, CallbackContext], object]
+) -> Callable[[Update, CallbackContext], object]:
+ """
+ Function decorator that will run the function in a new thread.
Will run :attr:`telegram.ext.Dispatcher.run_async`.
Using this decorator is only possible when only a single Dispatcher exist in the system.
- Note: Use this decorator to run handlers asynchronously.
+ Note:
+ DEPRECATED. Use :attr:`telegram.ext.Dispatcher.run_async` directly instead or the
+ :attr:`Handler.run_async` parameter.
+ Warning:
+ If you're using ``@run_async`` you cannot rely on adding custom attributes to
+ :class:`telegram.ext.CallbackContext`. See its docs for more info.
"""
+
@wraps(func)
- def async_func(*args, **kwargs):
- return Dispatcher.get_instance().run_async(func, *args, **kwargs)
+ def async_func(*args: object, **kwargs: object) -> object:
+ warnings.warn(
+ 'The @run_async decorator is deprecated. Use the `run_async` parameter of '
+ 'your Handler or `Dispatcher.run_async` instead.',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+ return Dispatcher.get_instance()._run_async( # pylint: disable=W0212
+ func, *args, update=None, error_handling=False, **kwargs
+ )
return async_func
class DispatcherHandlerStop(Exception):
- """Raise this in handler to prevent execution any other handler (even in different group)."""
- pass
+ """
+ Raise this in handler to prevent execution of any other handler (even in different group).
+ In order to use this exception in a :class:`telegram.ext.ConversationHandler`, pass the
+ optional ``state`` parameter instead of returning the next state:
-class Dispatcher(object):
- """This class dispatches all kinds of updates to its registered handlers.
+ .. code-block:: python
+
+ def callback(update, context):
+ ...
+ raise DispatcherHandlerStop(next_state)
Attributes:
- bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers.
- update_queue (:obj:`Queue`): The synchronized queue that will contain the updates.
- job_queue (:class:`telegram.ext.JobQueue`): Optional. The :class:`telegram.ext.JobQueue`
- instance to pass onto handler callbacks.
- workers (:obj:`int`): Number of maximum concurrent worker threads for the ``@run_async``
- decorator.
+ state (:obj:`object`): Optional. The next state of the conversation.
+
+ Args:
+ state (:obj:`object`, optional): The next state of the conversation.
+ """
+
+ __slots__ = ('state',)
+
+ def __init__(self, state: object = None) -> None:
+ super().__init__()
+ self.state = state
+
+
+class Dispatcher(Generic[CCT, UD, CD, BD]):
+ """This class dispatches all kinds of updates to its registered handlers.
Args:
bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers.
@@ -78,62 +132,216 @@ class Dispatcher(object):
job_queue (:class:`telegram.ext.JobQueue`, optional): The :class:`telegram.ext.JobQueue`
instance to pass onto handler callbacks.
workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the
- ``@run_async`` decorator. defaults to 4.
+ ``@run_async`` decorator and :meth:`run_async`. Defaults to 4.
+ persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
+ store data that should be persistent over restarts.
+ use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback
+ API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`.
+ **New users**: set this to :obj:`True`.
+ context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance
+ of :class:`telegram.ext.ContextTypes` to customize the types used in the
+ ``context`` interface. If not passed, the defaults documented in
+ :class:`telegram.ext.ContextTypes` will be used.
+
+ .. versionadded:: 13.6
+
+ Attributes:
+ bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers.
+ update_queue (:obj:`Queue`): The synchronized queue that will contain the updates.
+ job_queue (:class:`telegram.ext.JobQueue`): Optional. The :class:`telegram.ext.JobQueue`
+ instance to pass onto handler callbacks.
+ workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the
+ ``@run_async`` decorator and :meth:`run_async`.
+ user_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the user.
+ chat_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the chat.
+ bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot.
+ persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
+ store data that should be persistent over restarts.
+ context_types (:class:`telegram.ext.ContextTypes`): Container for the types used
+ in the ``context`` interface.
+
+ .. versionadded:: 13.6
"""
+ # Allowing '__weakref__' creation here since we need it for the singleton
+ __slots__ = (
+ 'workers',
+ 'persistence',
+ 'use_context',
+ 'update_queue',
+ 'job_queue',
+ 'user_data',
+ 'chat_data',
+ 'bot_data',
+ '_update_persistence_lock',
+ 'handlers',
+ 'groups',
+ 'error_handlers',
+ 'running',
+ '__stop_event',
+ '__exception_event',
+ '__async_queue',
+ '__async_threads',
+ 'bot',
+ '__dict__',
+ '__weakref__',
+ 'context_types',
+ )
+
__singleton_lock = Lock()
__singleton_semaphore = BoundedSemaphore()
__singleton = None
logger = logging.getLogger(__name__)
- def __init__(self, bot, update_queue, workers=4, exception_event=None, job_queue=None):
+ @overload
+ def __init__(
+ self: 'Dispatcher[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]',
+ bot: 'Bot',
+ update_queue: Queue,
+ workers: int = 4,
+ exception_event: Event = None,
+ job_queue: 'JobQueue' = None,
+ persistence: BasePersistence = None,
+ use_context: bool = True,
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'Dispatcher[CCT, UD, CD, BD]',
+ bot: 'Bot',
+ update_queue: Queue,
+ workers: int = 4,
+ exception_event: Event = None,
+ job_queue: 'JobQueue' = None,
+ persistence: BasePersistence = None,
+ use_context: bool = True,
+ context_types: ContextTypes[CCT, UD, CD, BD] = None,
+ ):
+ ...
+
+ def __init__(
+ self,
+ bot: 'Bot',
+ update_queue: Queue,
+ workers: int = 4,
+ exception_event: Event = None,
+ job_queue: 'JobQueue' = None,
+ persistence: BasePersistence = None,
+ use_context: bool = True,
+ context_types: ContextTypes[CCT, UD, CD, BD] = None,
+ ):
self.bot = bot
self.update_queue = update_queue
self.job_queue = job_queue
self.workers = workers
+ self.use_context = use_context
+ self.context_types = cast(ContextTypes[CCT, UD, CD, BD], context_types or ContextTypes())
+
+ if not use_context:
+ warnings.warn(
+ 'Old Handler API is deprecated - see https://git.io/fxJuV for details',
+ TelegramDeprecationWarning,
+ stacklevel=3,
+ )
+
+ if self.workers < 1:
+ warnings.warn(
+ 'Asynchronous callbacks can not be processed without at least one worker thread.'
+ )
+
+ self.user_data: DefaultDict[int, UD] = defaultdict(self.context_types.user_data)
+ self.chat_data: DefaultDict[int, CD] = defaultdict(self.context_types.chat_data)
+ self.bot_data = self.context_types.bot_data()
+ self.persistence: Optional[BasePersistence] = None
+ self._update_persistence_lock = Lock()
+ if persistence:
+ if not isinstance(persistence, BasePersistence):
+ raise TypeError("persistence must be based on telegram.ext.BasePersistence")
+ self.persistence = persistence
+ self.persistence.set_bot(self.bot)
+ if self.persistence.store_user_data:
+ self.user_data = self.persistence.get_user_data()
+ if not isinstance(self.user_data, defaultdict):
+ raise ValueError("user_data must be of type defaultdict")
+ if self.persistence.store_chat_data:
+ self.chat_data = self.persistence.get_chat_data()
+ if not isinstance(self.chat_data, defaultdict):
+ raise ValueError("chat_data must be of type defaultdict")
+ if self.persistence.store_bot_data:
+ self.bot_data = self.persistence.get_bot_data()
+ if not isinstance(self.bot_data, self.context_types.bot_data):
+ raise ValueError(
+ f"bot_data must be of type {self.context_types.bot_data.__name__}"
+ )
+ if self.persistence.store_callback_data:
+ self.bot = cast(telegram.ext.extbot.ExtBot, self.bot)
+ persistent_data = self.persistence.get_callback_data()
+ if persistent_data is not None:
+ if not isinstance(persistent_data, tuple) and len(persistent_data) != 2:
+ raise ValueError('callback_data must be a 2-tuple')
+ self.bot.callback_data_cache = CallbackDataCache(
+ self.bot,
+ self.bot.callback_data_cache.maxsize,
+ persistent_data=persistent_data,
+ )
+ else:
+ self.persistence = None
- self.user_data = defaultdict(dict)
- """:obj:`dict`: A dictionary handlers can use to store data for the user."""
- self.chat_data = defaultdict(dict)
- """:obj:`dict`: A dictionary handlers can use to store data for the chat."""
- self.handlers = {}
+ self.handlers: Dict[int, List[Handler]] = {}
"""Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group."""
- self.groups = []
+ self.groups: List[int] = []
"""List[:obj:`int`]: A list with all groups."""
- self.error_handlers = []
- """List[:obj:`callable`]: A list of errorHandlers."""
+ self.error_handlers: Dict[Callable, Union[bool, DefaultValue]] = {}
+ """Dict[:obj:`callable`, :obj:`bool`]: A dict, where the keys are error handlers and the
+ values indicate whether they are to be run asynchronously."""
self.running = False
""":obj:`bool`: Indicates if this dispatcher is running."""
self.__stop_event = Event()
self.__exception_event = exception_event or Event()
- self.__async_queue = Queue()
- self.__async_threads = set()
+ self.__async_queue: Queue = Queue()
+ self.__async_threads: Set[Thread] = set()
# For backward compatibility, we allow a "singleton" mode for the dispatcher. When there's
# only one instance of Dispatcher, it will be possible to use the `run_async` decorator.
with self.__singleton_lock:
- if self.__singleton_semaphore.acquire(blocking=0):
+ if self.__singleton_semaphore.acquire(blocking=False): # pylint: disable=R1732
self._set_singleton(self)
else:
self._set_singleton(None)
- def _init_async_threads(self, base_name, workers):
- base_name = '{}_'.format(base_name) if base_name else ''
+ def __setattr__(self, key: str, value: object) -> None:
+ # Mangled names don't automatically apply in __setattr__ (see
+ # https://docs.python.org/3/tutorial/classes.html#private-variables), so we have to make
+ # it mangled so they don't raise TelegramDeprecationWarning unnecessarily
+ if key.startswith('__'):
+ key = f"_{self.__class__.__name__}{key}"
+ if issubclass(self.__class__, Dispatcher) and self.__class__ is not Dispatcher:
+ object.__setattr__(self, key, value)
+ return
+ set_new_attribute_deprecated(self, key, value)
+
+ @property
+ def exception_event(self) -> Event: # skipcq: PY-D0003
+ return self.__exception_event
+
+ def _init_async_threads(self, base_name: str, workers: int) -> None:
+ base_name = f'{base_name}_' if base_name else ''
for i in range(workers):
- thread = Thread(target=self._pooled, name='{}{}'.format(base_name, i))
+ thread = Thread(target=self._pooled, name=f'Bot:{self.bot.id}:worker:{base_name}{i}')
self.__async_threads.add(thread)
thread.start()
@classmethod
- def _set_singleton(cls, val):
+ def _set_singleton(cls, val: Optional['Dispatcher']) -> None:
cls.logger.debug('Setting singleton dispatcher as %s', val)
cls.__singleton = weakref.ref(val) if val else None
@classmethod
- def get_instance(cls):
+ def get_instance(cls) -> 'Dispatcher':
"""Get the singleton instance of this class.
Returns:
@@ -144,47 +352,95 @@ def get_instance(cls):
"""
if cls.__singleton is not None:
- return cls.__singleton() # pylint: disable=not-callable
- else:
- raise RuntimeError('{} not initialized or multiple instances exist'.format(
- cls.__name__))
+ return cls.__singleton() # type: ignore[return-value] # pylint: disable=not-callable
+ raise RuntimeError(f'{cls.__name__} not initialized or multiple instances exist')
- def _pooled(self):
- thr_name = current_thread().getName()
+ def _pooled(self) -> None:
+ thr_name = current_thread().name
while 1:
promise = self.__async_queue.get()
# If unpacking fails, the thread pool is being closed from Updater._join_async_threads
if not isinstance(promise, Promise):
- self.logger.debug("Closing run_async thread %s/%d", thr_name,
- len(self.__async_threads))
+ self.logger.debug(
+ "Closing run_async thread %s/%d", thr_name, len(self.__async_threads)
+ )
break
promise.run()
+
+ if not promise.exception:
+ self.update_persistence(update=promise.update)
+ continue
+
if isinstance(promise.exception, DispatcherHandlerStop):
self.logger.warning(
'DispatcherHandlerStop is not supported with async functions; func: %s',
- promise.pooled_function.__name__)
+ promise.pooled_function.__name__,
+ )
+ continue
- def run_async(self, func, *args, **kwargs):
- """Queue a function (with given args/kwargs) to be run asynchronously.
+ # Avoid infinite recursion of error handlers.
+ if promise.pooled_function in self.error_handlers:
+ self.logger.error('An uncaught error was raised while handling the error.')
+ continue
+
+ # Don't perform error handling for a `Promise` with deactivated error handling. This
+ # should happen only via the deprecated `@run_async` decorator or `Promises` created
+ # within error handlers
+ if not promise.error_handling:
+ self.logger.error('A promise with deactivated error handling raised an error.')
+ continue
+
+ # If we arrive here, an exception happened in the promise and was neither
+ # DispatcherHandlerStop nor raised by an error handler. So we can and must handle it
+ try:
+ self.dispatch_error(promise.update, promise.exception, promise=promise)
+ except Exception:
+ self.logger.exception('An uncaught error was raised while handling the error.')
+
+ def run_async(
+ self, func: Callable[..., object], *args: object, update: object = None, **kwargs: object
+ ) -> Promise:
+ """
+ Queue a function (with given args/kwargs) to be run asynchronously. Exceptions raised
+ by the function will be handled by the error handlers registered with
+ :meth:`add_error_handler`.
+
+ Warning:
+ * If you're using ``@run_async``/:meth:`run_async` you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+ * Calling a function through :meth:`run_async` from within an error handler can lead to
+ an infinite error handling loop.
Args:
func (:obj:`callable`): The function to run in the thread.
- *args (:obj:`tuple`, optional): Arguments to `func`.
- **kwargs (:obj:`dict`, optional): Keyword arguments to `func`.
+ *args (:obj:`tuple`, optional): Arguments to ``func``.
+ update (:class:`telegram.Update` | :obj:`object`, optional): The update associated with
+ the functions call. If passed, it will be available in the error handlers, in case
+ an exception is raised by :attr:`func`.
+ **kwargs (:obj:`dict`, optional): Keyword arguments to ``func``.
Returns:
Promise
"""
- # TODO: handle exception in async threads
- # set a threading.Event to notify caller thread
- promise = Promise(func, args, kwargs)
+ return self._run_async(func, *args, update=update, error_handling=True, **kwargs)
+
+ def _run_async(
+ self,
+ func: Callable[..., object],
+ *args: object,
+ update: object = None,
+ error_handling: bool = True,
+ **kwargs: object,
+ ) -> Promise:
+ # TODO: Remove error_handling parameter once we drop the @run_async decorator
+ promise = Promise(func, args, kwargs, update=update, error_handling=error_handling)
self.__async_queue.put(promise)
return promise
- def start(self, ready=None):
+ def start(self, ready: Event = None) -> None:
"""Thread target of thread 'dispatcher'.
Runs in background and processes the update queue.
@@ -205,7 +461,7 @@ def start(self, ready=None):
self.logger.error(msg)
raise TelegramError(msg)
- self._init_async_threads(uuid4(), self.workers)
+ self._init_async_threads(str(uuid4()), self.workers)
self.running = True
self.logger.debug('Dispatcher started')
@@ -220,18 +476,19 @@ def start(self, ready=None):
if self.__stop_event.is_set():
self.logger.debug('orderly stopping')
break
- elif self.__exception_event.is_set():
+ if self.__exception_event.is_set():
self.logger.critical('stopping due to exception in another thread')
break
continue
- self.logger.debug('Processing Update: %s' % update)
+ self.logger.debug('Processing Update: %s', update)
self.process_update(update)
+ self.update_queue.task_done()
self.running = False
self.logger.debug('Dispatcher thread stopped')
- def stop(self):
+ def stop(self) -> None:
"""Stops the thread."""
if self.running:
self.__stop_event.set()
@@ -249,20 +506,27 @@ def stop(self):
self.__async_queue.put(None)
for i, thr in enumerate(threads):
- self.logger.debug('Waiting for async thread {0}/{1} to end'.format(i + 1, total))
+ self.logger.debug('Waiting for async thread %s/%s to end', i + 1, total)
thr.join()
self.__async_threads.remove(thr)
- self.logger.debug('async thread {0}/{1} has ended'.format(i + 1, total))
+ self.logger.debug('async thread %s/%s has ended', i + 1, total)
@property
- def has_running_threads(self):
+ def has_running_threads(self) -> bool: # skipcq: PY-D0003
return self.running or bool(self.__async_threads)
- def process_update(self, update):
- """Processes a single update.
+ def process_update(self, update: object) -> None:
+ """Processes a single update and updates the persistence.
+
+ Note:
+ If the update is handled by least one synchronously running handlers (i.e.
+ ``run_async=False``), :meth:`update_persistence` is called *once* after all handlers
+ synchronous handlers are done. Each asynchronously running handler will trigger
+ :meth:`update_persistence` on its own.
Args:
- update (:obj:`str` | :class:`telegram.Update` | :class:`telegram.TelegramError`):
+ update (:class:`telegram.Update` | :obj:`object` | \
+ :class:`telegram.error.TelegramError`):
The update to process.
"""
@@ -271,40 +535,58 @@ def process_update(self, update):
try:
self.dispatch_error(None, update)
except Exception:
- self.logger.exception('An uncaught error was raised while handling the error')
+ self.logger.exception('An uncaught error was raised while handling the error.')
return
+ context = None
+ handled = False
+ sync_modes = []
+
for group in self.groups:
try:
- for handler in (x for x in self.handlers[group] if x.check_update(update)):
- handler.handle_update(update, self)
- break
+ for handler in self.handlers[group]:
+ check = handler.check_update(update)
+ if check is not None and check is not False:
+ if not context and self.use_context:
+ context = self.context_types.context.from_update(update, self)
+ context.refresh_data()
+ handled = True
+ sync_modes.append(handler.run_async)
+ handler.handle_update(update, self, check, context)
+ break
# Stop processing with any other handler.
except DispatcherHandlerStop:
self.logger.debug('Stopping further handlers due to DispatcherHandlerStop')
+ self.update_persistence(update=update)
break
# Dispatch any error.
- except TelegramError as te:
- self.logger.warning('A TelegramError was raised while processing the Update')
-
+ except Exception as exc:
try:
- self.dispatch_error(update, te)
+ self.dispatch_error(update, exc)
except DispatcherHandlerStop:
self.logger.debug('Error handler stopped further handlers')
break
+ # Errors should not stop the thread.
except Exception:
- self.logger.exception('An uncaught error was raised while handling the error')
-
- # Errors should not stop the thread.
- except Exception:
- self.logger.exception('An uncaught error was raised while processing the update')
-
- def add_handler(self, handler, group=DEFAULT_GROUP):
+ self.logger.exception('An uncaught error was raised while handling the error.')
+
+ # Update persistence, if handled
+ handled_only_async = all(sync_modes)
+ if handled:
+ # Respect default settings
+ if all(mode is DEFAULT_FALSE for mode in sync_modes) and self.bot.defaults:
+ handled_only_async = self.bot.defaults.run_async
+ # If update was only handled by async handlers, we don't need to update here
+ if not handled_only_async:
+ self.update_persistence(update=update)
+
+ def add_handler(self, handler: Handler[UT, CCT], group: int = DEFAULT_GROUP) -> None:
"""Register a handler.
- TL;DR: Order and priority counts. 0 or 1 handlers per group will be used.
+ TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of
+ update with :class:`telegram.ext.DispatcherHandlerStop`.
A handler must be an instance of a subclass of :class:`telegram.ext.Handler`. All handlers
are organized in groups with a numeric value. The default group is 0. All groups will be
@@ -325,20 +607,38 @@ def add_handler(self, handler, group=DEFAULT_GROUP):
group (:obj:`int`, optional): The group identifier. Default is 0.
"""
+ # Unfortunately due to circular imports this has to be here
+ from .conversationhandler import ConversationHandler # pylint: disable=C0415
if not isinstance(handler, Handler):
- raise TypeError('handler is not an instance of {0}'.format(Handler.__name__))
+ raise TypeError(f'handler is not an instance of {Handler.__name__}')
if not isinstance(group, int):
raise TypeError('group is not int')
+ # For some reason MyPy infers the type of handler is here,
+ # so for now we just ignore all the errors
+ if (
+ isinstance(handler, ConversationHandler)
+ and handler.persistent # type: ignore[attr-defined]
+ and handler.name # type: ignore[attr-defined]
+ ):
+ if not self.persistence:
+ raise ValueError(
+ f"ConversationHandler {handler.name} " # type: ignore[attr-defined]
+ f"can not be persistent if dispatcher has no persistence"
+ )
+ handler.persistence = self.persistence # type: ignore[attr-defined]
+ handler.conversations = ( # type: ignore[attr-defined]
+ self.persistence.get_conversations(handler.name) # type: ignore[attr-defined]
+ )
if group not in self.handlers:
- self.handlers[group] = list()
+ self.handlers[group] = []
self.groups.append(group)
self.groups = sorted(self.groups)
self.handlers[group].append(handler)
- def remove_handler(self, handler, group=DEFAULT_GROUP):
+ def remove_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None:
"""Remove a handler from the specified group.
Args:
@@ -352,38 +652,169 @@ def remove_handler(self, handler, group=DEFAULT_GROUP):
del self.handlers[group]
self.groups.remove(group)
- def add_error_handler(self, callback):
- """Registers an error handler in the Dispatcher.
+ def update_persistence(self, update: object = None) -> None:
+ """Update :attr:`user_data`, :attr:`chat_data` and :attr:`bot_data` in :attr:`persistence`.
+
+ Args:
+ update (:class:`telegram.Update`, optional): The update to process. If passed, only the
+ corresponding ``user_data`` and ``chat_data`` will be updated.
+ """
+ with self._update_persistence_lock:
+ self.__update_persistence(update)
+
+ def __update_persistence(self, update: object = None) -> None:
+ if self.persistence:
+ # We use list() here in order to decouple chat_ids from self.chat_data, as dict view
+ # objects will change, when the dict does and we want to loop over chat_ids
+ chat_ids = list(self.chat_data.keys())
+ user_ids = list(self.user_data.keys())
+
+ if isinstance(update, Update):
+ if update.effective_chat:
+ chat_ids = [update.effective_chat.id]
+ else:
+ chat_ids = []
+ if update.effective_user:
+ user_ids = [update.effective_user.id]
+ else:
+ user_ids = []
+
+ if self.persistence.store_callback_data:
+ self.bot = cast(telegram.ext.extbot.ExtBot, self.bot)
+ try:
+ self.persistence.update_callback_data(
+ self.bot.callback_data_cache.persistence_data
+ )
+ except Exception as exc:
+ try:
+ self.dispatch_error(update, exc)
+ except Exception:
+ message = (
+ 'Saving callback data raised an error and an '
+ 'uncaught error was raised while handling '
+ 'the error with an error_handler'
+ )
+ self.logger.exception(message)
+ if self.persistence.store_bot_data:
+ try:
+ self.persistence.update_bot_data(self.bot_data)
+ except Exception as exc:
+ try:
+ self.dispatch_error(update, exc)
+ except Exception:
+ message = (
+ 'Saving bot data raised an error and an '
+ 'uncaught error was raised while handling '
+ 'the error with an error_handler'
+ )
+ self.logger.exception(message)
+ if self.persistence.store_chat_data:
+ for chat_id in chat_ids:
+ try:
+ self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
+ except Exception as exc:
+ try:
+ self.dispatch_error(update, exc)
+ except Exception:
+ message = (
+ 'Saving chat data raised an error and an '
+ 'uncaught error was raised while handling '
+ 'the error with an error_handler'
+ )
+ self.logger.exception(message)
+ if self.persistence.store_user_data:
+ for user_id in user_ids:
+ try:
+ self.persistence.update_user_data(user_id, self.user_data[user_id])
+ except Exception as exc:
+ try:
+ self.dispatch_error(update, exc)
+ except Exception:
+ message = (
+ 'Saving user data raised an error and an '
+ 'uncaught error was raised while handling '
+ 'the error with an error_handler'
+ )
+ self.logger.exception(message)
+
+ def add_error_handler(
+ self,
+ callback: Callable[[object, CCT], None],
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, # pylint: disable=W0621
+ ) -> None:
+ """Registers an error handler in the Dispatcher. This handler will receive every error
+ which happens in your bot.
+
+ Note:
+ Attempts to add the same callback multiple times will be ignored.
+
+ Warning:
+ The errors handled within these handlers won't show up in the logger, so you
+ need to make sure that you reraise the error.
Args:
- callback (:obj:`callable`): A function that takes ``Bot, Update, TelegramError`` as
- arguments.
+ callback (:obj:`callable`): The callback function for this error handler. Will be
+ called when an error is raised. Callback signature for context based API:
+ ``def callback(update: object, context: CallbackContext)``
+
+ The error that happened will be present in context.error.
+ run_async (:obj:`bool`, optional): Whether this handlers callback should be run
+ asynchronously using :meth:`run_async`. Defaults to :obj:`False`.
+
+ Note:
+ See https://git.io/fxJuV for more info about switching to context based API.
"""
- self.error_handlers.append(callback)
+ if callback in self.error_handlers:
+ self.logger.debug('The callback is already registered as an error handler. Ignoring.')
+ return
- def remove_error_handler(self, callback):
+ if run_async is DEFAULT_FALSE and self.bot.defaults and self.bot.defaults.run_async:
+ run_async = True
+
+ self.error_handlers[callback] = run_async
+
+ def remove_error_handler(self, callback: Callable[[object, CCT], None]) -> None:
"""Removes an error handler.
Args:
callback (:obj:`callable`): The error handler to remove.
"""
- if callback in self.error_handlers:
- self.error_handlers.remove(callback)
+ self.error_handlers.pop(callback, None)
- def dispatch_error(self, update, error):
+ def dispatch_error(
+ self, update: Optional[object], error: Exception, promise: Promise = None
+ ) -> None:
"""Dispatches an error.
Args:
- update (:obj:`str` | :class:`telegram.Update` | None): The update that caused the error
- error (:class:`telegram.TelegramError`): The Telegram error that was raised.
+ update (:obj:`object` | :class:`telegram.Update`): The update that caused the error.
+ error (:obj:`Exception`): The error that was raised.
+ promise (:class:`telegram.utils.Promise`, optional): The promise whose pooled function
+ raised the error.
"""
+ async_args = None if not promise else promise.args
+ async_kwargs = None if not promise else promise.kwargs
+
if self.error_handlers:
- for callback in self.error_handlers:
- callback(self.bot, update, error)
+ for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621
+ if self.use_context:
+ context = self.context_types.context.from_error(
+ update, error, self, async_args=async_args, async_kwargs=async_kwargs
+ )
+ if run_async:
+ self.run_async(callback, update, context, update=update)
+ else:
+ callback(update, context)
+ else:
+ if run_async:
+ self.run_async(callback, self.bot, update, error, update=update)
+ else:
+ callback(self.bot, update, error)
else:
self.logger.exception(
- 'No error handlers are registered, logging exception.', exc_info=error)
+ 'No error handlers are registered, logging exception.', exc_info=error
+ )
diff --git a/telegramer/include/telegram/ext/extbot.py b/telegramer/include/telegram/ext/extbot.py
new file mode 100644
index 0000000..f026191
--- /dev/null
+++ b/telegramer/include/telegram/ext/extbot.py
@@ -0,0 +1,341 @@
+#!/usr/bin/env python
+# pylint: disable=E0611,E0213,E1102,C0103,E1101,R0913,R0904
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram Bot with convenience extensions."""
+from copy import copy
+from typing import Union, cast, List, Callable, Optional, Tuple, TypeVar, TYPE_CHECKING, Sequence
+
+import telegram.bot
+from telegram import (
+ ReplyMarkup,
+ Message,
+ InlineKeyboardMarkup,
+ Poll,
+ MessageId,
+ Update,
+ Chat,
+ CallbackQuery,
+)
+
+from telegram.ext.callbackdatacache import CallbackDataCache
+from telegram.utils.types import JSONDict, ODVInput, DVInput
+from ..utils.helpers import DEFAULT_NONE
+
+if TYPE_CHECKING:
+ from telegram import InlineQueryResult, MessageEntity
+ from telegram.utils.request import Request
+ from .defaults import Defaults
+
+HandledTypes = TypeVar('HandledTypes', bound=Union[Message, CallbackQuery, Chat])
+
+
+class ExtBot(telegram.bot.Bot):
+ """This object represents a Telegram Bot with convenience extensions.
+
+ Warning:
+ Not to be confused with :class:`telegram.Bot`.
+
+ For the documentation of the arguments, methods and attributes, please see
+ :class:`telegram.Bot`.
+
+ .. versionadded:: 13.6
+
+ Args:
+ defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to
+ be used if not set explicitly in the bot methods.
+ arbitrary_callback_data (:obj:`bool` | :obj:`int`, optional): Whether to
+ allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`.
+ Pass an integer to specify the maximum number of objects cached in memory. For more
+ details, please see our `wiki `_. Defaults to :obj:`False`.
+
+ Attributes:
+ arbitrary_callback_data (:obj:`bool` | :obj:`int`): Whether this bot instance
+ allows to use arbitrary objects as callback data for
+ :class:`telegram.InlineKeyboardButton`.
+ callback_data_cache (:class:`telegram.ext.CallbackDataCache`): The cache for objects passed
+ as callback data for :class:`telegram.InlineKeyboardButton`.
+
+ """
+
+ __slots__ = ('arbitrary_callback_data', 'callback_data_cache')
+
+ # The ext_bot argument is a little hack to get warnings handled correctly.
+ # It's not very clean, but the warnings will be dropped at some point anyway.
+ def __setattr__(self, key: str, value: object, ext_bot: bool = True) -> None:
+ if issubclass(self.__class__, ExtBot) and self.__class__ is not ExtBot:
+ object.__setattr__(self, key, value)
+ return
+ super().__setattr__(key, value, ext_bot=ext_bot) # type: ignore[call-arg]
+
+ def __init__(
+ self,
+ token: str,
+ base_url: str = None,
+ base_file_url: str = None,
+ request: 'Request' = None,
+ private_key: bytes = None,
+ private_key_password: bytes = None,
+ defaults: 'Defaults' = None,
+ arbitrary_callback_data: Union[bool, int] = False,
+ ):
+ super().__init__(
+ token=token,
+ base_url=base_url,
+ base_file_url=base_file_url,
+ request=request,
+ private_key=private_key,
+ private_key_password=private_key_password,
+ )
+ # We don't pass this to super().__init__ to avoid the deprecation warning
+ self.defaults = defaults
+
+ # set up callback_data
+ if not isinstance(arbitrary_callback_data, bool):
+ maxsize = cast(int, arbitrary_callback_data)
+ self.arbitrary_callback_data = True
+ else:
+ maxsize = 1024
+ self.arbitrary_callback_data = arbitrary_callback_data
+ self.callback_data_cache: CallbackDataCache = CallbackDataCache(bot=self, maxsize=maxsize)
+
+ def _replace_keyboard(self, reply_markup: Optional[ReplyMarkup]) -> Optional[ReplyMarkup]:
+ # If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the
+ # CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input
+ if isinstance(reply_markup, InlineKeyboardMarkup) and self.arbitrary_callback_data:
+ return self.callback_data_cache.process_keyboard(reply_markup)
+
+ return reply_markup
+
+ def insert_callback_data(self, update: Update) -> None:
+ """If this bot allows for arbitrary callback data, this inserts the cached data into all
+ corresponding buttons within this update.
+
+ Note:
+ Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to check
+ if the reply markup (if any) was actually sent by this caches bot. If it was not, the
+ message will be returned unchanged.
+
+ Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is
+ :obj:`None` for those! In the corresponding reply markups the callback data will be
+ replaced by :class:`telegram.ext.InvalidCallbackData`.
+
+ Warning:
+ *In place*, i.e. the passed :class:`telegram.Message` will be changed!
+
+ Args:
+ update (:class`telegram.Update`): The update.
+
+ """
+ # The only incoming updates that can directly contain a message sent by the bot itself are:
+ # * CallbackQueries
+ # * Messages where the pinned_message is sent by the bot
+ # * Messages where the reply_to_message is sent by the bot
+ # * Messages where via_bot is the bot
+ # Finally there is effective_chat.pinned message, but that's only returned in get_chat
+ if update.callback_query:
+ self._insert_callback_data(update.callback_query)
+ # elif instead of if, as effective_message includes callback_query.message
+ # and that has already been processed
+ elif update.effective_message:
+ self._insert_callback_data(update.effective_message)
+
+ def _insert_callback_data(self, obj: HandledTypes) -> HandledTypes:
+ if not self.arbitrary_callback_data:
+ return obj
+
+ if isinstance(obj, CallbackQuery):
+ self.callback_data_cache.process_callback_query(obj)
+ return obj # type: ignore[return-value]
+
+ if isinstance(obj, Message):
+ if obj.reply_to_message:
+ # reply_to_message can't contain further reply_to_messages, so no need to check
+ self.callback_data_cache.process_message(obj.reply_to_message)
+ if obj.reply_to_message.pinned_message:
+ # pinned messages can't contain reply_to_message, no need to check
+ self.callback_data_cache.process_message(obj.reply_to_message.pinned_message)
+ if obj.pinned_message:
+ # pinned messages can't contain reply_to_message, no need to check
+ self.callback_data_cache.process_message(obj.pinned_message)
+
+ # Finally, handle the message itself
+ self.callback_data_cache.process_message(message=obj)
+ return obj # type: ignore[return-value]
+
+ if isinstance(obj, Chat) and obj.pinned_message:
+ self.callback_data_cache.process_message(obj.pinned_message)
+
+ return obj
+
+ def _message(
+ self,
+ endpoint: str,
+ data: JSONDict,
+ reply_to_message_id: int = None,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_markup: ReplyMarkup = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> Union[bool, Message]:
+ # We override this method to call self._replace_keyboard and self._insert_callback_data.
+ # This covers most methods that have a reply_markup
+ result = super()._message(
+ endpoint=endpoint,
+ data=data,
+ reply_to_message_id=reply_to_message_id,
+ disable_notification=disable_notification,
+ reply_markup=self._replace_keyboard(reply_markup),
+ allow_sending_without_reply=allow_sending_without_reply,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+ if isinstance(result, Message):
+ self._insert_callback_data(result)
+ return result
+
+ def get_updates(
+ self,
+ offset: int = None,
+ limit: int = 100,
+ timeout: float = 0,
+ read_latency: float = 2.0,
+ allowed_updates: List[str] = None,
+ api_kwargs: JSONDict = None,
+ ) -> List[Update]:
+ updates = super().get_updates(
+ offset=offset,
+ limit=limit,
+ timeout=timeout,
+ read_latency=read_latency,
+ allowed_updates=allowed_updates,
+ api_kwargs=api_kwargs,
+ )
+
+ for update in updates:
+ self.insert_callback_data(update)
+
+ return updates
+
+ def _effective_inline_results( # pylint: disable=R0201
+ self,
+ results: Union[
+ Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]]
+ ],
+ next_offset: str = None,
+ current_offset: str = None,
+ ) -> Tuple[Sequence['InlineQueryResult'], Optional[str]]:
+ """
+ This method is called by Bot.answer_inline_query to build the actual results list.
+ Overriding this to call self._replace_keyboard suffices
+ """
+ effective_results, next_offset = super()._effective_inline_results(
+ results=results, next_offset=next_offset, current_offset=current_offset
+ )
+
+ # Process arbitrary callback
+ if not self.arbitrary_callback_data:
+ return effective_results, next_offset
+ results = []
+ for result in effective_results:
+ # All currently existingInlineQueryResults have a reply_markup, but future ones
+ # might not have. Better be save than sorry
+ if not hasattr(result, 'reply_markup'):
+ results.append(result)
+ else:
+ # We build a new result in case the user wants to use the same object in
+ # different places
+ new_result = copy(result)
+ markup = self._replace_keyboard(result.reply_markup) # type: ignore[attr-defined]
+ new_result.reply_markup = markup
+ results.append(new_result)
+
+ return results, next_offset
+
+ def stop_poll(
+ self,
+ chat_id: Union[int, str],
+ message_id: int,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Poll:
+ # We override this method to call self._replace_keyboard
+ return super().stop_poll(
+ chat_id=chat_id,
+ message_id=message_id,
+ reply_markup=self._replace_keyboard(reply_markup),
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def copy_message(
+ self,
+ chat_id: Union[int, str],
+ from_chat_id: Union[str, int],
+ message_id: int,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> MessageId:
+ # We override this method to call self._replace_keyboard
+ return super().copy_message(
+ chat_id=chat_id,
+ from_chat_id=from_chat_id,
+ message_id=message_id,
+ caption=caption,
+ parse_mode=parse_mode,
+ caption_entities=caption_entities,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ allow_sending_without_reply=allow_sending_without_reply,
+ reply_markup=self._replace_keyboard(reply_markup),
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ def get_chat(
+ self,
+ chat_id: Union[str, int],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Chat:
+ # We override this method to call self._insert_callback_data
+ result = super().get_chat(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs)
+ return self._insert_callback_data(result)
+
+ # updated camelCase aliases
+ getChat = get_chat
+ """Alias for :meth:`get_chat`"""
+ copyMessage = copy_message
+ """Alias for :meth:`copy_message`"""
+ getUpdates = get_updates
+ """Alias for :meth:`get_updates`"""
+ stopPoll = stop_poll
+ """Alias for :meth:`stop_poll`"""
diff --git a/telegramer/include/telegram/ext/filters.py b/telegramer/include/telegram/ext/filters.py
index cbef3eb..519c73a 100644
--- a/telegramer/include/telegram/ext/filters.py
+++ b/telegramer/include/telegram/ext/filters.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,26 +16,51 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=C0112, C0103, W0221
"""This module contains the Filters for use with the MessageHandler class."""
import re
-from telegram import Chat
-# REMREM from future.utils import string_types
-try:
- from future.utils import string_types
-except Exception as e:
- pass
-
-try:
- string_types
-except NameError:
- string_types = str
-
-
-class BaseFilter(object):
- """Base class for all Message Filters.
-
- Subclassing from this class filters to be combined using bitwise operators:
+import warnings
+
+from abc import ABC, abstractmethod
+from sys import version_info as py_ver
+from threading import Lock
+from typing import (
+ Dict,
+ FrozenSet,
+ List,
+ Match,
+ Optional,
+ Pattern,
+ Set,
+ Tuple,
+ Union,
+ cast,
+ NoReturn,
+)
+
+from telegram import Chat, Message, MessageEntity, Update, User
+
+__all__ = [
+ 'Filters',
+ 'BaseFilter',
+ 'MessageFilter',
+ 'UpdateFilter',
+ 'InvertedFilter',
+ 'MergedFilter',
+ 'XORFilter',
+]
+
+from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated
+from telegram.utils.types import SLT
+
+DataDict = Dict[str, list]
+
+
+class BaseFilter(ABC):
+ """Base class for all Filters.
+
+ Filters subclassing from this class can combined using bitwise operators:
And:
@@ -45,6 +70,10 @@ class BaseFilter(object):
>>> (Filters.audio | Filters.video)
+ Exclusive Or:
+
+ >>> (Filters.regex('To Be') ^ Filters.regex('Not 2B'))
+
Not:
>>> ~ Filters.command
@@ -54,55 +83,169 @@ class BaseFilter(object):
>>> (Filters.text & (Filters.entity(URL) | Filters.entity(TEXT_LINK)))
>>> Filters.text & (~ Filters.forwarded)
- If you want to create your own filters create a class inheriting from this class and implement
- a `filter` method that returns a boolean: `True` if the message should be handled, `False`
- otherwise. Note that the filters work only as class instances, not actual class objects
- (so remember to initialize your filter classes).
+ Note:
+ Filters use the same short circuiting logic as python's `and`, `or` and `not`.
+ This means that for example:
+
+ >>> Filters.regex(r'(a?x)') | Filters.regex(r'(b?x)')
+
+ With ``message.text == x``, will only ever return the matches for the first filter,
+ since the second one is never evaluated.
+
+
+ If you want to create your own filters create a class inheriting from either
+ :class:`MessageFilter` or :class:`UpdateFilter` and implement a :meth:`filter` method that
+ returns a boolean: :obj:`True` if the message should be
+ handled, :obj:`False` otherwise.
+ Note that the filters work only as class instances, not
+ actual class objects (so remember to
+ initialize your filter classes).
By default the filters name (what will get printed when converted to a string for display)
- will be the class name. If you want to overwrite this assign a better name to the `name`
+ will be the class name. If you want to overwrite this assign a better name to the :attr:`name`
class variable.
Attributes:
name (:obj:`str`): Name for this filter. Defaults to the type of filter.
-
+ data_filter (:obj:`bool`): Whether this filter is a data filter. A data filter should
+ return a dict with lists. The dict will be merged with
+ :class:`telegram.ext.CallbackContext`'s internal dict in most cases
+ (depends on the handler).
"""
- name = None
+ if py_ver < (3, 7):
+ __slots__ = ('_name', '_data_filter')
+ else:
+ __slots__ = ('_name', '_data_filter', '__dict__') # type: ignore[assignment]
+
+ def __new__(cls, *args: object, **kwargs: object) -> 'BaseFilter': # pylint: disable=W0613
+ instance = super().__new__(cls)
+ instance._name = None
+ instance._data_filter = False
+
+ return instance
- def __call__(self, message):
- return self.filter(message)
+ @abstractmethod
+ def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]:
+ ...
- def __and__(self, other):
+ def __and__(self, other: 'BaseFilter') -> 'BaseFilter':
return MergedFilter(self, and_filter=other)
- def __or__(self, other):
+ def __or__(self, other: 'BaseFilter') -> 'BaseFilter':
return MergedFilter(self, or_filter=other)
- def __invert__(self):
+ def __xor__(self, other: 'BaseFilter') -> 'BaseFilter':
+ return XORFilter(self, other)
+
+ def __invert__(self) -> 'BaseFilter':
return InvertedFilter(self)
- def __repr__(self):
+ def __setattr__(self, key: str, value: object) -> None:
+ # Allow setting custom attributes w/o warning for user defined custom filters.
+ # To differentiate between a custom and a PTB filter, we use this hacky but
+ # simple way of checking the module name where the class is defined from.
+ if (
+ issubclass(self.__class__, (UpdateFilter, MessageFilter))
+ and self.__class__.__module__ != __name__
+ ): # __name__ is telegram.ext.filters
+ object.__setattr__(self, key, value)
+ return
+ set_new_attribute_deprecated(self, key, value)
+
+ @property
+ def data_filter(self) -> bool:
+ return self._data_filter
+
+ @data_filter.setter
+ def data_filter(self, value: bool) -> None:
+ self._data_filter = value
+
+ @property
+ def name(self) -> Optional[str]:
+ return self._name
+
+ @name.setter
+ def name(self, name: Optional[str]) -> None:
+ self._name = name # pylint: disable=E0237
+
+ def __repr__(self) -> str:
# We do this here instead of in a __init__ so filter don't have to call __init__ or super()
if self.name is None:
self.name = self.__class__.__name__
return self.name
- def filter(self, message):
+
+class MessageFilter(BaseFilter):
+ """Base class for all Message Filters. In contrast to :class:`UpdateFilter`, the object passed
+ to :meth:`filter` is ``update.effective_message``.
+
+ Please see :class:`telegram.ext.filters.BaseFilter` for details on how to create custom
+ filters.
+
+ Attributes:
+ name (:obj:`str`): Name for this filter. Defaults to the type of filter.
+ data_filter (:obj:`bool`): Whether this filter is a data filter. A data filter should
+ return a dict with lists. The dict will be merged with
+ :class:`telegram.ext.CallbackContext`'s internal dict in most cases
+ (depends on the handler).
+
+ """
+
+ __slots__ = ()
+
+ def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]:
+ return self.filter(update.effective_message)
+
+ @abstractmethod
+ def filter(self, message: Message) -> Optional[Union[bool, DataDict]]:
"""This method must be overwritten.
Args:
message (:class:`telegram.Message`): The message that is tested.
Returns:
- :obj:`bool`
+ :obj:`dict` or :obj:`bool`
"""
- raise NotImplementedError
+class UpdateFilter(BaseFilter):
+ """Base class for all Update Filters. In contrast to :class:`MessageFilter`, the object
+ passed to :meth:`filter` is ``update``, which allows to create filters like
+ :attr:`Filters.update.edited_message`.
+
+ Please see :class:`telegram.ext.filters.BaseFilter` for details on how to create custom
+ filters.
-class InvertedFilter(BaseFilter):
+ Attributes:
+ name (:obj:`str`): Name for this filter. Defaults to the type of filter.
+ data_filter (:obj:`bool`): Whether this filter is a data filter. A data filter should
+ return a dict with lists. The dict will be merged with
+ :class:`telegram.ext.CallbackContext`'s internal dict in most cases
+ (depends on the handler).
+
+ """
+
+ __slots__ = ()
+
+ def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]:
+ return self.filter(update)
+
+ @abstractmethod
+ def filter(self, update: Update) -> Optional[Union[bool, DataDict]]:
+ """This method must be overwritten.
+
+ Args:
+ update (:class:`telegram.Update`): The update that is tested.
+
+ Returns:
+ :obj:`dict` or :obj:`bool`.
+
+ """
+
+
+class InvertedFilter(UpdateFilter):
"""Represents a filter that has been inverted.
Args:
@@ -110,44 +253,183 @@ class InvertedFilter(BaseFilter):
"""
- def __init__(self, f):
+ __slots__ = ('f',)
+
+ def __init__(self, f: BaseFilter):
self.f = f
- def filter(self, message):
- return not self.f(message)
+ def filter(self, update: Update) -> bool:
+ return not bool(self.f(update))
+
+ @property
+ def name(self) -> str:
+ return f""
- def __repr__(self):
- return "".format(self.f)
+ @name.setter
+ def name(self, name: str) -> NoReturn:
+ raise RuntimeError('Cannot set name for InvertedFilter')
-class MergedFilter(BaseFilter):
+class MergedFilter(UpdateFilter):
"""Represents a filter consisting of two other filters.
Args:
- base_filter: Filter 1 of the merged filter
+ base_filter: Filter 1 of the merged filter.
and_filter: Optional filter to "and" with base_filter. Mutually exclusive with or_filter.
or_filter: Optional filter to "or" with base_filter. Mutually exclusive with and_filter.
"""
- def __init__(self, base_filter, and_filter=None, or_filter=None):
+ __slots__ = ('base_filter', 'and_filter', 'or_filter')
+
+ def __init__(
+ self, base_filter: BaseFilter, and_filter: BaseFilter = None, or_filter: BaseFilter = None
+ ):
self.base_filter = base_filter
+ if self.base_filter.data_filter:
+ self.data_filter = True
self.and_filter = and_filter
+ if (
+ self.and_filter
+ and not isinstance(self.and_filter, bool)
+ and self.and_filter.data_filter
+ ):
+ self.data_filter = True
self.or_filter = or_filter
-
- def filter(self, message):
+ if self.or_filter and not isinstance(self.and_filter, bool) and self.or_filter.data_filter:
+ self.data_filter = True
+
+ @staticmethod
+ def _merge(base_output: Union[bool, Dict], comp_output: Union[bool, Dict]) -> DataDict:
+ base = base_output if isinstance(base_output, dict) else {}
+ comp = comp_output if isinstance(comp_output, dict) else {}
+ for k in comp.keys():
+ # Make sure comp values are lists
+ comp_value = comp[k] if isinstance(comp[k], list) else []
+ try:
+ # If base is a list then merge
+ if isinstance(base[k], list):
+ base[k] += comp_value
+ else:
+ base[k] = [base[k]] + comp_value
+ except KeyError:
+ base[k] = comp_value
+ return base
+
+ def filter(self, update: Update) -> Union[bool, DataDict]: # pylint: disable=R0911
+ base_output = self.base_filter(update)
+ # We need to check if the filters are data filters and if so return the merged data.
+ # If it's not a data filter or an or_filter but no matches return bool
if self.and_filter:
- return self.base_filter(message) and self.and_filter(message)
+ # And filter needs to short circuit if base is falsey
+ if base_output:
+ comp_output = self.and_filter(update)
+ if comp_output:
+ if self.data_filter:
+ merged = self._merge(base_output, comp_output)
+ if merged:
+ return merged
+ return True
elif self.or_filter:
- return self.base_filter(message) or self.or_filter(message)
+ # Or filter needs to short circuit if base is truthey
+ if base_output:
+ if self.data_filter:
+ return base_output
+ return True
+
+ comp_output = self.or_filter(update)
+ if comp_output:
+ if self.data_filter:
+ return comp_output
+ return True
+ return False
+
+ @property
+ def name(self) -> str:
+ return (
+ f"<{self.base_filter} {'and' if self.and_filter else 'or'} "
+ f"{self.and_filter or self.or_filter}>"
+ )
+
+ @name.setter
+ def name(self, name: str) -> NoReturn:
+ raise RuntimeError('Cannot set name for MergedFilter')
+
+
+class XORFilter(UpdateFilter):
+ """Convenience filter acting as wrapper for :class:`MergedFilter` representing the an XOR gate
+ for two filters.
+
+ Args:
+ base_filter: Filter 1 of the merged filter.
+ xor_filter: Filter 2 of the merged filter.
+
+ """
- def __repr__(self):
- return "<{} {} {}>".format(self.base_filter, "and" if self.and_filter else "or",
- self.and_filter or self.or_filter)
+ __slots__ = ('base_filter', 'xor_filter', 'merged_filter')
+ def __init__(self, base_filter: BaseFilter, xor_filter: BaseFilter):
+ self.base_filter = base_filter
+ self.xor_filter = xor_filter
+ self.merged_filter = (base_filter & ~xor_filter) | (~base_filter & xor_filter)
+
+ def filter(self, update: Update) -> Optional[Union[bool, DataDict]]:
+ return self.merged_filter(update)
+
+ @property
+ def name(self) -> str:
+ return f'<{self.base_filter} xor {self.xor_filter}>'
+
+ @name.setter
+ def name(self, name: str) -> NoReturn:
+ raise RuntimeError('Cannot set name for XORFilter')
+
+
+class _DiceEmoji(MessageFilter):
+ __slots__ = ('emoji',)
+
+ def __init__(self, emoji: str = None, name: str = None):
+ self.name = f'Filters.dice.{name}' if name else 'Filters.dice'
+ self.emoji = emoji
+
+ class _DiceValues(MessageFilter):
+ __slots__ = ('values', 'emoji')
+
+ def __init__(
+ self,
+ values: SLT[int],
+ name: str,
+ emoji: str = None,
+ ):
+ self.values = [values] if isinstance(values, int) else values
+ self.emoji = emoji
+ self.name = f'{name}({values})'
+
+ def filter(self, message: Message) -> bool:
+ if message.dice and message.dice.value in self.values:
+ if self.emoji:
+ return message.dice.emoji == self.emoji
+ return True
+ return False
+
+ def __call__( # type: ignore[override]
+ self, update: Union[Update, List[int], Tuple[int]]
+ ) -> Union[bool, '_DiceValues']:
+ if isinstance(update, Update):
+ return self.filter(update.effective_message)
+ return self._DiceValues(update, self.name, emoji=self.emoji)
+
+ def filter(self, message: Message) -> bool:
+ if bool(message.dice):
+ if self.emoji:
+ return message.dice.emoji == self.emoji
+ return True
+ return False
-class Filters(object):
- """Predefined filters for use as the `filter` argument of :class:`telegram.ext.MessageHandler`.
+
+class Filters:
+ """Predefined filters for use as the ``filter`` argument of
+ :class:`telegram.ext.MessageHandler`.
Examples:
Use ``MessageHandler(Filters.video, callback_method)`` to filter all video
@@ -155,85 +437,272 @@ class Filters(object):
"""
- class _All(BaseFilter):
+ __slots__ = ('__dict__',)
+
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
+
+ class _All(MessageFilter):
+ __slots__ = ()
name = 'Filters.all'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return True
all = _All()
- """:obj:`Filter`: All Messages."""
+ """All Messages."""
- class _Text(BaseFilter):
+ class _Text(MessageFilter):
+ __slots__ = ()
name = 'Filters.text'
- def filter(self, message):
- return bool(message.text and not message.text.startswith('/'))
+ class _TextStrings(MessageFilter):
+ __slots__ = ('strings',)
+
+ def __init__(self, strings: Union[List[str], Tuple[str]]):
+ self.strings = strings
+ self.name = f'Filters.text({strings})'
+
+ def filter(self, message: Message) -> bool:
+ if message.text:
+ return message.text in self.strings
+ return False
+
+ def __call__( # type: ignore[override]
+ self, update: Union[Update, List[str], Tuple[str]]
+ ) -> Union[bool, '_TextStrings']:
+ if isinstance(update, Update):
+ return self.filter(update.effective_message)
+ return self._TextStrings(update)
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.text)
text = _Text()
- """:obj:`Filter`: Text Messages."""
+ """Text Messages. If a list of strings is passed, it filters messages to only allow those
+ whose text is appearing in the given list.
+
+ Examples:
+ To allow any text message, simply use
+ ``MessageHandler(Filters.text, callback_method)``.
+
+ A simple use case for passing a list is to allow only messages that were sent by a
+ custom :class:`telegram.ReplyKeyboardMarkup`::
+
+ buttons = ['Start', 'Settings', 'Back']
+ markup = ReplyKeyboardMarkup.from_column(buttons)
+ ...
+ MessageHandler(Filters.text(buttons), callback_method)
+
+ Note:
+ * Dice messages don't have text. If you want to filter either text or dice messages, use
+ ``Filters.text | Filters.dice``.
+ * Messages containing a command are accepted by this filter. Use
+ ``Filters.text & (~Filters.command)``, if you want to filter only text messages without
+ commands.
+
+ Args:
+ update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only
+ exact matches are allowed. If not specified, will allow any text message.
+ """
+
+ class _Caption(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.caption'
+
+ class _CaptionStrings(MessageFilter):
+ __slots__ = ('strings',)
+
+ def __init__(self, strings: Union[List[str], Tuple[str]]):
+ self.strings = strings
+ self.name = f'Filters.caption({strings})'
+
+ def filter(self, message: Message) -> bool:
+ if message.caption:
+ return message.caption in self.strings
+ return False
+
+ def __call__( # type: ignore[override]
+ self, update: Union[Update, List[str], Tuple[str]]
+ ) -> Union[bool, '_CaptionStrings']:
+ if isinstance(update, Update):
+ return self.filter(update.effective_message)
+ return self._CaptionStrings(update)
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.caption)
- class _Command(BaseFilter):
+ caption = _Caption()
+ """Messages with a caption. If a list of strings is passed, it filters messages to only
+ allow those whose caption is appearing in the given list.
+
+ Examples:
+ ``MessageHandler(Filters.caption, callback_method)``
+
+ Args:
+ update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which captions to allow. Only
+ exact matches are allowed. If not specified, will allow any message with a caption.
+ """
+
+ class _Command(MessageFilter):
+ __slots__ = ()
name = 'Filters.command'
- def filter(self, message):
- return bool(message.text and message.text.startswith('/'))
+ class _CommandOnlyStart(MessageFilter):
+ __slots__ = ('only_start',)
+
+ def __init__(self, only_start: bool):
+ self.only_start = only_start
+ self.name = f'Filters.command({only_start})'
+
+ def filter(self, message: Message) -> bool:
+ return bool(
+ message.entities
+ and any(e.type == MessageEntity.BOT_COMMAND for e in message.entities)
+ )
+
+ def __call__( # type: ignore[override]
+ self, update: Union[bool, Update]
+ ) -> Union[bool, '_CommandOnlyStart']:
+ if isinstance(update, Update):
+ return self.filter(update.effective_message)
+ return self._CommandOnlyStart(update)
+
+ def filter(self, message: Message) -> bool:
+ return bool(
+ message.entities
+ and message.entities[0].type == MessageEntity.BOT_COMMAND
+ and message.entities[0].offset == 0
+ )
command = _Command()
- """:obj:`Filter`: Messages starting with ``/``."""
+ """
+ Messages with a :attr:`telegram.MessageEntity.BOT_COMMAND`. By default only allows
+ messages `starting` with a bot command. Pass :obj:`False` to also allow messages that contain a
+ bot command `anywhere` in the text.
+
+ Examples::
+
+ MessageHandler(Filters.command, command_at_start_callback)
+ MessageHandler(Filters.command(False), command_anywhere_callback)
+
+ Note:
+ ``Filters.text`` also accepts messages containing a command.
+
+ Args:
+ update (:obj:`bool`, optional): Whether to only allow messages that `start` with a bot
+ command. Defaults to :obj:`True`.
+ """
- class regex(BaseFilter):
+ class regex(MessageFilter):
"""
- Filters updates by searching for an occurence of ``pattern`` in the message text.
- The ``re.search`` function is used to determine whether an update should be filtered.
+ Filters updates by searching for an occurrence of ``pattern`` in the message text.
+ The ``re.search()`` function is used to determine whether an update should be filtered.
+
Refer to the documentation of the ``re`` module for more information.
- Note: Does not allow passing groups or a groupdict like the ``RegexHandler`` yet,
- but this will probably be implemented in a future update, gradually phasing out the
- RegexHandler (see https://github.com/python-telegram-bot/python-telegram-bot/issues/835).
+ To get the groups and groupdict matched, see :attr:`telegram.ext.CallbackContext.matches`.
Examples:
- Example ``CommandHandler("start", deep_linked_callback, Filters.regex('parameter'))``
+ Use ``MessageHandler(Filters.regex(r'help'), callback)`` to capture all messages that
+ contain the word 'help'. You can also use
+ ``MessageHandler(Filters.regex(re.compile(r'help', re.IGNORECASE)), callback)`` if
+ you want your pattern to be case insensitive. This approach is recommended
+ if you need to specify flags on your pattern.
+
+ Note:
+ Filters use the same short circuiting logic as python's `and`, `or` and `not`.
+ This means that for example:
+
+ >>> Filters.regex(r'(a?x)') | Filters.regex(r'(b?x)')
+
+ With a message.text of `x`, will only ever return the matches for the first filter,
+ since the second one is never evaluated.
Args:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
"""
- def __init__(self, pattern):
- self.pattern = re.compile(pattern)
- self.name = 'Filters.regex({})'.format(self.pattern)
+ __slots__ = ('pattern',)
+ data_filter = True
- # TODO: Once the callback revamp (#1026) is done, the regex filter should be able to pass
- # the matched groups and groupdict to the context object.
+ def __init__(self, pattern: Union[str, Pattern]):
+ if isinstance(pattern, str):
+ pattern = re.compile(pattern)
+ pattern = cast(Pattern, pattern)
+ self.pattern: Pattern = pattern
+ self.name = f'Filters.regex({self.pattern})'
- def filter(self, message):
+ def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]:
+ """""" # remove method from docs
if message.text:
- return bool(self.pattern.search(message.text))
- return False
+ match = self.pattern.search(message.text)
+ if match:
+ return {'matches': [match]}
+ return {}
+
+ class caption_regex(MessageFilter):
+ """
+ Filters updates by searching for an occurrence of ``pattern`` in the message caption.
+
+ This filter works similarly to :class:`Filters.regex`, with the only exception being that
+ it applies to the message caption instead of the text.
+
+ Examples:
+ Use ``MessageHandler(Filters.photo & Filters.caption_regex(r'help'), callback)``
+ to capture all photos with caption containing the word 'help'.
+
+ Note:
+ This filter will not work on simple text messages, but only on media with caption.
- class _Reply(BaseFilter):
+ Args:
+ pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
+ """
+
+ __slots__ = ('pattern',)
+ data_filter = True
+
+ def __init__(self, pattern: Union[str, Pattern]):
+ if isinstance(pattern, str):
+ pattern = re.compile(pattern)
+ pattern = cast(Pattern, pattern)
+ self.pattern: Pattern = pattern
+ self.name = f'Filters.caption_regex({self.pattern})'
+
+ def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]:
+ """""" # remove method from docs
+ if message.caption:
+ match = self.pattern.search(message.caption)
+ if match:
+ return {'matches': [match]}
+ return {}
+
+ class _Reply(MessageFilter):
+ __slots__ = ()
name = 'Filters.reply'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.reply_to_message)
reply = _Reply()
- """:obj:`Filter`: Messages that are a reply to another message."""
+ """Messages that are a reply to another message."""
- class _Audio(BaseFilter):
+ class _Audio(MessageFilter):
+ __slots__ = ()
name = 'Filters.audio'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.audio)
audio = _Audio()
- """:obj:`Filter`: Messages that contain :class:`telegram.Audio`."""
+ """Messages that contain :class:`telegram.Audio`."""
- class _Document(BaseFilter):
+ class _Document(MessageFilter):
+ __slots__ = ()
name = 'Filters.document'
- class category(BaseFilter):
- """This Filter filters documents by their category in the mime-type attribute
+ class category(MessageFilter):
+ """Filters documents by their category in the mime-type attribute.
Note:
This Filter only filters by the mime_type of the document,
@@ -241,22 +710,27 @@ class category(BaseFilter):
The user can manipulate the mime-type of a message and
send media with wrong types that don't fit to this handler.
- Examples:
- Filters.documents.category('audio/') returnes `True` for all types
- of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'
+ Example:
+ Filters.document.category('audio/') returns :obj:`True` for all types
+ of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'.
"""
- def __init__(self, category):
+ __slots__ = ('_category',)
+
+ def __init__(self, category: Optional[str]):
"""Initialize the category you want to filter
Args:
- category (str, optional): category of the media you want to filter"""
- self.category = category
- self.name = "Filters.document.category('{}')".format(self.category)
+ category (str, optional): category of the media you want to filter
+ """
+ self._category = category
+ self.name = f"Filters.document.category('{self._category}')"
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
+ """""" # remove method from docs
if message.document:
- return message.document.mime_type.startswith(self.category)
+ return message.document.mime_type.startswith(self._category)
+ return False
application = category('application/')
audio = category('audio/')
@@ -264,7 +738,7 @@ def filter(self, message):
video = category('video/')
text = category('text/')
- class mime_type(BaseFilter):
+ class mime_type(MessageFilter):
"""This Filter filters documents by their mime-type attribute
Note:
@@ -273,21 +747,21 @@ class mime_type(BaseFilter):
The user can manipulate the mime-type of a message and
send media with wrong types that don't fit to this handler.
- Examples:
- Filters.documents.mime_type('audio/mpeg') filters all audio in mp3 format.
+ Example:
+ ``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format.
"""
- def __init__(self, mimetype):
- """Initialize the category you want to filter
+ __slots__ = ('mimetype',)
- Args:
- filetype (str, optional): mime_type of the media you want to filter"""
+ def __init__(self, mimetype: Optional[str]):
self.mimetype = mimetype
- self.name = "Filters.document.mime_type('{}')".format(self.mimetype)
+ self.name = f"Filters.document.mime_type('{self.mimetype}')"
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
+ """""" # remove method from docs
if message.document:
return message.document.mime_type == self.mimetype
+ return False
apk = mime_type('application/vnd.android.package-archive')
doc = mime_type('application/msword')
@@ -305,94 +779,242 @@ def filter(self, message):
xml = mime_type('application/xml')
zip = mime_type('application/zip')
- def filter(self, message):
+ class file_extension(MessageFilter):
+ """This filter filters documents by their file ending/extension.
+
+ Note:
+ * This Filter only filters by the file ending/extension of the document,
+ it doesn't check the validity of document.
+ * The user can manipulate the file extension of a document and
+ send media with wrong types that don't fit to this handler.
+ * Case insensitive by default,
+ you may change this with the flag ``case_sensitive=True``.
+ * Extension should be passed without leading dot
+ unless it's a part of the extension.
+ * Pass :obj:`None` to filter files with no extension,
+ i.e. without a dot in the filename.
+
+ Example:
+ * ``Filters.document.file_extension("jpg")``
+ filters files with extension ``".jpg"``.
+ * ``Filters.document.file_extension(".jpg")``
+ filters files with extension ``"..jpg"``.
+ * ``Filters.document.file_extension("Dockerfile", case_sensitive=True)``
+ filters files with extension ``".Dockerfile"`` minding the case.
+ * ``Filters.document.file_extension(None)``
+ filters files without a dot in the filename.
+ """
+
+ __slots__ = ('_file_extension', 'is_case_sensitive')
+
+ def __init__(self, file_extension: Optional[str], case_sensitive: bool = False):
+ """Initialize the extension you want to filter.
+
+ Args:
+ file_extension (:obj:`str` | :obj:`None`):
+ media file extension you want to filter.
+ case_sensitive (:obj:bool, optional):
+ pass :obj:`True` to make the filter case sensitive.
+ Default: :obj:`False`.
+ """
+ self.is_case_sensitive = case_sensitive
+ if file_extension is None:
+ self._file_extension = None
+ self.name = "Filters.document.file_extension(None)"
+ elif self.is_case_sensitive:
+ self._file_extension = f".{file_extension}"
+ self.name = (
+ f"Filters.document.file_extension({file_extension!r},"
+ " case_sensitive=True)"
+ )
+ else:
+ self._file_extension = f".{file_extension}".lower()
+ self.name = f"Filters.document.file_extension({file_extension.lower()!r})"
+
+ def filter(self, message: Message) -> bool:
+ """""" # remove method from docs
+ if message.document is None:
+ return False
+ if self._file_extension is None:
+ return "." not in message.document.file_name
+ if self.is_case_sensitive:
+ filename = message.document.file_name
+ else:
+ filename = message.document.file_name.lower()
+ return filename.endswith(self._file_extension)
+
+ def filter(self, message: Message) -> bool:
return bool(message.document)
document = _Document()
- """:obj:`Filter`: Messages that contain :class:`telegram.Document`."""
+ """
+ Subset for messages containing a document/file.
+
+ Examples:
+ Use these filters like: ``Filters.document.mp3``,
+ ``Filters.document.mime_type("text/plain")`` etc. Or use just
+ ``Filters.document`` for all document messages.
+
+ Attributes:
+ category: Filters documents by their category in the mime-type attribute
- class _Animation(BaseFilter):
+ Note:
+ This Filter only filters by the mime_type of the document,
+ it doesn't check the validity of the document.
+ The user can manipulate the mime-type of a message and
+ send media with wrong types that don't fit to this handler.
+
+ Example:
+ ``Filters.document.category('audio/')`` filters all types
+ of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'.
+ application: Same as ``Filters.document.category("application")``.
+ audio: Same as ``Filters.document.category("audio")``.
+ image: Same as ``Filters.document.category("image")``.
+ video: Same as ``Filters.document.category("video")``.
+ text: Same as ``Filters.document.category("text")``.
+ mime_type: Filters documents by their mime-type attribute
+
+ Note:
+ This Filter only filters by the mime_type of the document,
+ it doesn't check the validity of document.
+
+ The user can manipulate the mime-type of a message and
+ send media with wrong types that don't fit to this handler.
+
+ Example:
+ ``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format.
+ apk: Same as ``Filters.document.mime_type("application/vnd.android.package-archive")``.
+ doc: Same as ``Filters.document.mime_type("application/msword")``.
+ docx: Same as ``Filters.document.mime_type("application/vnd.openxmlformats-\
+officedocument.wordprocessingml.document")``.
+ exe: Same as ``Filters.document.mime_type("application/x-ms-dos-executable")``.
+ gif: Same as ``Filters.document.mime_type("video/mp4")``.
+ jpg: Same as ``Filters.document.mime_type("image/jpeg")``.
+ mp3: Same as ``Filters.document.mime_type("audio/mpeg")``.
+ pdf: Same as ``Filters.document.mime_type("application/pdf")``.
+ py: Same as ``Filters.document.mime_type("text/x-python")``.
+ svg: Same as ``Filters.document.mime_type("image/svg+xml")``.
+ txt: Same as ``Filters.document.mime_type("text/plain")``.
+ targz: Same as ``Filters.document.mime_type("application/x-compressed-tar")``.
+ wav: Same as ``Filters.document.mime_type("audio/x-wav")``.
+ xml: Same as ``Filters.document.mime_type("application/xml")``.
+ zip: Same as ``Filters.document.mime_type("application/zip")``.
+ file_extension: This filter filters documents by their file ending/extension.
+
+ Note:
+ * This Filter only filters by the file ending/extension of the document,
+ it doesn't check the validity of document.
+ * The user can manipulate the file extension of a document and
+ send media with wrong types that don't fit to this handler.
+ * Case insensitive by default,
+ you may change this with the flag ``case_sensitive=True``.
+ * Extension should be passed without leading dot
+ unless it's a part of the extension.
+ * Pass :obj:`None` to filter files with no extension,
+ i.e. without a dot in the filename.
+
+ Example:
+ * ``Filters.document.file_extension("jpg")``
+ filters files with extension ``".jpg"``.
+ * ``Filters.document.file_extension(".jpg")``
+ filters files with extension ``"..jpg"``.
+ * ``Filters.document.file_extension("Dockerfile", case_sensitive=True)``
+ filters files with extension ``".Dockerfile"`` minding the case.
+ * ``Filters.document.file_extension(None)``
+ filters files without a dot in the filename.
+ """
+
+ class _Animation(MessageFilter):
+ __slots__ = ()
name = 'Filters.animation'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.animation)
animation = _Animation()
- """:obj:`Filter`: Messages that contain :class:`telegram.Animation`."""
+ """Messages that contain :class:`telegram.Animation`."""
- class _Photo(BaseFilter):
+ class _Photo(MessageFilter):
+ __slots__ = ()
name = 'Filters.photo'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.photo)
photo = _Photo()
- """:obj:`Filter`: Messages that contain :class:`telegram.PhotoSize`."""
+ """Messages that contain :class:`telegram.PhotoSize`."""
- class _Sticker(BaseFilter):
+ class _Sticker(MessageFilter):
+ __slots__ = ()
name = 'Filters.sticker'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.sticker)
sticker = _Sticker()
- """:obj:`Filter`: Messages that contain :class:`telegram.Sticker`."""
+ """Messages that contain :class:`telegram.Sticker`."""
- class _Video(BaseFilter):
+ class _Video(MessageFilter):
+ __slots__ = ()
name = 'Filters.video'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.video)
video = _Video()
- """:obj:`Filter`: Messages that contain :class:`telegram.Video`."""
+ """Messages that contain :class:`telegram.Video`."""
- class _Voice(BaseFilter):
+ class _Voice(MessageFilter):
+ __slots__ = ()
name = 'Filters.voice'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.voice)
voice = _Voice()
- """:obj:`Filter`: Messages that contain :class:`telegram.Voice`."""
+ """Messages that contain :class:`telegram.Voice`."""
- class _VideoNote(BaseFilter):
+ class _VideoNote(MessageFilter):
+ __slots__ = ()
name = 'Filters.video_note'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.video_note)
video_note = _VideoNote()
- """:obj:`Filter`: Messages that contain :class:`telegram.VideoNote`."""
+ """Messages that contain :class:`telegram.VideoNote`."""
- class _Contact(BaseFilter):
+ class _Contact(MessageFilter):
+ __slots__ = ()
name = 'Filters.contact'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.contact)
contact = _Contact()
- """:obj:`Filter`: Messages that contain :class:`telegram.Contact`."""
+ """Messages that contain :class:`telegram.Contact`."""
- class _Location(BaseFilter):
+ class _Location(MessageFilter):
+ __slots__ = ()
name = 'Filters.location'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.location)
location = _Location()
- """:obj:`Filter`: Messages that contain :class:`telegram.Location`."""
+ """Messages that contain :class:`telegram.Location`."""
- class _Venue(BaseFilter):
+ class _Venue(MessageFilter):
+ __slots__ = ()
name = 'Filters.venue'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.venue)
venue = _Venue()
- """:obj:`Filter`: Messages that contain :class:`telegram.Venue`."""
+ """Messages that contain :class:`telegram.Venue`."""
- class _StatusUpdate(BaseFilter):
+ class _StatusUpdate(UpdateFilter):
"""Subset for messages containing a status update.
Examples:
@@ -401,99 +1023,185 @@ class _StatusUpdate(BaseFilter):
"""
- class _NewChatMembers(BaseFilter):
+ __slots__ = ()
+
+ class _NewChatMembers(MessageFilter):
+ __slots__ = ()
name = 'Filters.status_update.new_chat_members'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.new_chat_members)
new_chat_members = _NewChatMembers()
- """:obj:`Filter`: Messages that contain :attr:`telegram.Message.new_chat_members`."""
+ """Messages that contain :attr:`telegram.Message.new_chat_members`."""
- class _LeftChatMember(BaseFilter):
+ class _LeftChatMember(MessageFilter):
+ __slots__ = ()
name = 'Filters.status_update.left_chat_member'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.left_chat_member)
left_chat_member = _LeftChatMember()
- """:obj:`Filter`: Messages that contain :attr:`telegram.Message.left_chat_member`."""
+ """Messages that contain :attr:`telegram.Message.left_chat_member`."""
- class _NewChatTitle(BaseFilter):
+ class _NewChatTitle(MessageFilter):
+ __slots__ = ()
name = 'Filters.status_update.new_chat_title'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.new_chat_title)
new_chat_title = _NewChatTitle()
- """:obj:`Filter`: Messages that contain :attr:`telegram.Message.new_chat_title`."""
+ """Messages that contain :attr:`telegram.Message.new_chat_title`."""
- class _NewChatPhoto(BaseFilter):
+ class _NewChatPhoto(MessageFilter):
+ __slots__ = ()
name = 'Filters.status_update.new_chat_photo'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.new_chat_photo)
new_chat_photo = _NewChatPhoto()
- """:obj:`Filter`: Messages that contain :attr:`telegram.Message.new_chat_photo`."""
+ """Messages that contain :attr:`telegram.Message.new_chat_photo`."""
- class _DeleteChatPhoto(BaseFilter):
+ class _DeleteChatPhoto(MessageFilter):
+ __slots__ = ()
name = 'Filters.status_update.delete_chat_photo'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.delete_chat_photo)
delete_chat_photo = _DeleteChatPhoto()
- """:obj:`Filter`: Messages that contain :attr:`telegram.Message.delete_chat_photo`."""
+ """Messages that contain :attr:`telegram.Message.delete_chat_photo`."""
- class _ChatCreated(BaseFilter):
+ class _ChatCreated(MessageFilter):
+ __slots__ = ()
name = 'Filters.status_update.chat_created'
- def filter(self, message):
- return bool(message.group_chat_created or message.supergroup_chat_created or
- message.channel_chat_created)
+ def filter(self, message: Message) -> bool:
+ return bool(
+ message.group_chat_created
+ or message.supergroup_chat_created
+ or message.channel_chat_created
+ )
chat_created = _ChatCreated()
- """:obj:`Filter`: Messages that contain :attr:`telegram.Message.group_chat_created`,
+ """Messages that contain :attr:`telegram.Message.group_chat_created`,
:attr: `telegram.Message.supergroup_chat_created` or
:attr: `telegram.Message.channel_chat_created`."""
- class _Migrate(BaseFilter):
+ class _MessageAutoDeleteTimerChanged(MessageFilter):
+ __slots__ = ()
+ name = 'MessageAutoDeleteTimerChanged'
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.message_auto_delete_timer_changed)
+
+ message_auto_delete_timer_changed = _MessageAutoDeleteTimerChanged()
+ """Messages that contain :attr:`message_auto_delete_timer_changed`"""
+
+ class _Migrate(MessageFilter):
+ __slots__ = ()
name = 'Filters.status_update.migrate'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.migrate_from_chat_id or message.migrate_to_chat_id)
migrate = _Migrate()
- """:obj:`Filter`: Messages that contain :attr:`telegram.Message.migrate_from_chat_id` or
- :attr: `telegram.Message.migrate_to_chat_id`."""
+ """Messages that contain :attr:`telegram.Message.migrate_from_chat_id` or
+ :attr:`telegram.Message.migrate_to_chat_id`."""
- class _PinnedMessage(BaseFilter):
+ class _PinnedMessage(MessageFilter):
+ __slots__ = ()
name = 'Filters.status_update.pinned_message'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.pinned_message)
pinned_message = _PinnedMessage()
- """:obj:`Filter`: Messages that contain :attr:`telegram.Message.pinned_message`."""
+ """Messages that contain :attr:`telegram.Message.pinned_message`."""
- class _ConnectedWebsite(BaseFilter):
+ class _ConnectedWebsite(MessageFilter):
+ __slots__ = ()
name = 'Filters.status_update.connected_website'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.connected_website)
connected_website = _ConnectedWebsite()
- """:obj:`Filter`: Messages that contain :attr:`telegram.Message.connected_website`."""
+ """Messages that contain :attr:`telegram.Message.connected_website`."""
+
+ class _ProximityAlertTriggered(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.status_update.proximity_alert_triggered'
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.proximity_alert_triggered)
+
+ proximity_alert_triggered = _ProximityAlertTriggered()
+ """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`."""
+
+ class _VoiceChatScheduled(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.status_update.voice_chat_scheduled'
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.voice_chat_scheduled)
+
+ voice_chat_scheduled = _VoiceChatScheduled()
+ """Messages that contain :attr:`telegram.Message.voice_chat_scheduled`."""
+
+ class _VoiceChatStarted(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.status_update.voice_chat_started'
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.voice_chat_started)
+
+ voice_chat_started = _VoiceChatStarted()
+ """Messages that contain :attr:`telegram.Message.voice_chat_started`."""
+
+ class _VoiceChatEnded(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.status_update.voice_chat_ended'
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.voice_chat_ended)
+
+ voice_chat_ended = _VoiceChatEnded()
+ """Messages that contain :attr:`telegram.Message.voice_chat_ended`."""
+
+ class _VoiceChatParticipantsInvited(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.status_update.voice_chat_participants_invited'
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.voice_chat_participants_invited)
+
+ voice_chat_participants_invited = _VoiceChatParticipantsInvited()
+ """Messages that contain :attr:`telegram.Message.voice_chat_participants_invited`."""
name = 'Filters.status_update'
- def filter(self, message):
- return bool(self.new_chat_members(message) or self.left_chat_member(message) or
- self.new_chat_title(message) or self.new_chat_photo(message) or
- self.delete_chat_photo(message) or self.chat_created(message) or
- self.migrate(message) or self.pinned_message(message) or
- self.connected_website(message))
+ def filter(self, message: Update) -> bool:
+ return bool(
+ self.new_chat_members(message)
+ or self.left_chat_member(message)
+ or self.new_chat_title(message)
+ or self.new_chat_photo(message)
+ or self.delete_chat_photo(message)
+ or self.chat_created(message)
+ or self.message_auto_delete_timer_changed(message)
+ or self.migrate(message)
+ or self.pinned_message(message)
+ or self.connected_website(message)
+ or self.proximity_alert_triggered(message)
+ or self.voice_chat_scheduled(message)
+ or self.voice_chat_started(message)
+ or self.voice_chat_ended(message)
+ or self.voice_chat_participants_invited(message)
+ )
status_update = _StatusUpdate()
"""Subset for messages containing a status update.
@@ -503,46 +1211,73 @@ def filter(self, message):
``Filters.status_update`` for all status update messages.
Attributes:
- chat_created (:obj:`Filter`): Messages that contain
+ chat_created: Messages that contain
:attr:`telegram.Message.group_chat_created`,
:attr:`telegram.Message.supergroup_chat_created` or
:attr:`telegram.Message.channel_chat_created`.
- delete_chat_photo (:obj:`Filter`): Messages that contain
+ connected_website: Messages that contain
+ :attr:`telegram.Message.connected_website`.
+ delete_chat_photo: Messages that contain
:attr:`telegram.Message.delete_chat_photo`.
- left_chat_member (:obj:`Filter`): Messages that contain
+ left_chat_member: Messages that contain
:attr:`telegram.Message.left_chat_member`.
- migrate (:obj:`Filter`): Messages that contain
- :attr:`telegram.Message.migrate_from_chat_id` or
- :attr: `telegram.Message.migrate_from_chat_id`.
- new_chat_members (:obj:`Filter`): Messages that contain
+ migrate: Messages that contain
+ :attr:`telegram.Message.migrate_to_chat_id` or
+ :attr:`telegram.Message.migrate_from_chat_id`.
+ new_chat_members: Messages that contain
:attr:`telegram.Message.new_chat_members`.
- new_chat_photo (:obj:`Filter`): Messages that contain
+ new_chat_photo: Messages that contain
:attr:`telegram.Message.new_chat_photo`.
- new_chat_title (:obj:`Filter`): Messages that contain
+ new_chat_title: Messages that contain
:attr:`telegram.Message.new_chat_title`.
- pinned_message (:obj:`Filter`): Messages that contain
+ message_auto_delete_timer_changed: Messages that contain
+ :attr:`message_auto_delete_timer_changed`.
+
+ .. versionadded:: 13.4
+ pinned_message: Messages that contain
:attr:`telegram.Message.pinned_message`.
+ proximity_alert_triggered: Messages that contain
+ :attr:`telegram.Message.proximity_alert_triggered`.
+ voice_chat_scheduled: Messages that contain
+ :attr:`telegram.Message.voice_chat_scheduled`.
+
+ .. versionadded:: 13.5
+ voice_chat_started: Messages that contain
+ :attr:`telegram.Message.voice_chat_started`.
+
+ .. versionadded:: 13.4
+ voice_chat_ended: Messages that contain
+ :attr:`telegram.Message.voice_chat_ended`.
+
+ .. versionadded:: 13.4
+ voice_chat_participants_invited: Messages that contain
+ :attr:`telegram.Message.voice_chat_participants_invited`.
+
+ .. versionadded:: 13.4
+
"""
- class _Forwarded(BaseFilter):
+ class _Forwarded(MessageFilter):
+ __slots__ = ()
name = 'Filters.forwarded'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.forward_date)
forwarded = _Forwarded()
- """:obj:`Filter`: Messages that are forwarded."""
+ """Messages that are forwarded."""
- class _Game(BaseFilter):
+ class _Game(MessageFilter):
+ __slots__ = ()
name = 'Filters.game'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.game)
game = _Game()
- """:obj:`Filter`: Messages that contain :class:`telegram.Game`."""
+ """Messages that contain :class:`telegram.Game`."""
- class entity(BaseFilter):
+ class entity(MessageFilter):
"""
Filters messages to only allow those which have a :class:`telegram.MessageEntity`
where their `type` matches `entity_type`.
@@ -556,14 +1291,17 @@ class entity(BaseFilter):
"""
- def __init__(self, entity_type):
+ __slots__ = ('entity_type',)
+
+ def __init__(self, entity_type: str):
self.entity_type = entity_type
- self.name = 'Filters.entity({})'.format(self.entity_type)
+ self.name = f'Filters.entity({self.entity_type})'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
+ """""" # remove method from docs
return any(entity.type == self.entity_type for entity in message.entities)
- class caption_entity(BaseFilter):
+ class caption_entity(MessageFilter):
"""
Filters media messages to only allow those which have a :class:`telegram.MessageEntity`
where their `type` matches `entity_type`.
@@ -577,156 +1315,1028 @@ class caption_entity(BaseFilter):
"""
- def __init__(self, entity_type):
+ __slots__ = ('entity_type',)
+
+ def __init__(self, entity_type: str):
self.entity_type = entity_type
- self.name = 'Filters.caption_entity({})'.format(self.entity_type)
+ self.name = f'Filters.caption_entity({self.entity_type})'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
+ """""" # remove method from docs
return any(entity.type == self.entity_type for entity in message.caption_entities)
- class _Private(BaseFilter):
+ class _Private(MessageFilter):
+ __slots__ = ()
name = 'Filters.private'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
+ warnings.warn(
+ 'Filters.private is deprecated. Use Filters.chat_type.private instead.',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
return message.chat.type == Chat.PRIVATE
private = _Private()
- """:obj:`Filter`: Messages sent in a private chat."""
+ """
+ Messages sent in a private chat.
- class _Group(BaseFilter):
+ Note:
+ DEPRECATED. Use
+ :attr:`telegram.ext.Filters.chat_type.private` instead.
+ """
+
+ class _Group(MessageFilter):
+ __slots__ = ()
name = 'Filters.group'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
+ warnings.warn(
+ 'Filters.group is deprecated. Use Filters.chat_type.groups instead.',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP]
group = _Group()
- """:obj:`Filter`: Messages sent in a group chat."""
+ """
+ Messages sent in a group or a supergroup chat.
+
+ Note:
+ DEPRECATED. Use
+ :attr:`telegram.ext.Filters.chat_type.groups` instead.
+ """
+
+ class _ChatType(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.chat_type'
+
+ class _Channel(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.chat_type.channel'
+
+ def filter(self, message: Message) -> bool:
+ return message.chat.type == Chat.CHANNEL
+
+ channel = _Channel()
+
+ class _Group(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.chat_type.group'
+
+ def filter(self, message: Message) -> bool:
+ return message.chat.type == Chat.GROUP
+
+ group = _Group()
+
+ class _SuperGroup(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.chat_type.supergroup'
+
+ def filter(self, message: Message) -> bool:
+ return message.chat.type == Chat.SUPERGROUP
+
+ supergroup = _SuperGroup()
- class user(BaseFilter):
- """Filters messages to allow only those which are from specified user ID.
+ class _Groups(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.chat_type.groups'
+
+ def filter(self, message: Message) -> bool:
+ return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP]
+
+ groups = _Groups()
+
+ class _Private(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.chat_type.private'
+
+ def filter(self, message: Message) -> bool:
+ return message.chat.type == Chat.PRIVATE
+
+ private = _Private()
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.chat.type)
+
+ chat_type = _ChatType()
+ """Subset for filtering the type of chat.
+
+ Examples:
+ Use these filters like: ``Filters.chat_type.channel`` or
+ ``Filters.chat_type.supergroup`` etc. Or use just ``Filters.chat_type`` for all
+ chat types.
+
+ Attributes:
+ channel: Updates from channel
+ group: Updates from group
+ supergroup: Updates from supergroup
+ groups: Updates from group *or* supergroup
+ private: Updates sent in private chat
+ """
+
+ class _ChatUserBaseFilter(MessageFilter, ABC):
+ __slots__ = (
+ 'chat_id_name',
+ 'username_name',
+ 'allow_empty',
+ '__lock',
+ '_chat_ids',
+ '_usernames',
+ )
+
+ def __init__(
+ self,
+ chat_id: SLT[int] = None,
+ username: SLT[str] = None,
+ allow_empty: bool = False,
+ ):
+ self.chat_id_name = 'chat_id'
+ self.username_name = 'username'
+ self.allow_empty = allow_empty
+ self.__lock = Lock()
+
+ self._chat_ids: Set[int] = set()
+ self._usernames: Set[str] = set()
+
+ self._set_chat_ids(chat_id)
+ self._set_usernames(username)
+
+ @abstractmethod
+ def get_chat_or_user(self, message: Message) -> Union[Chat, User, None]:
+ ...
+
+ @staticmethod
+ def _parse_chat_id(chat_id: SLT[int]) -> Set[int]:
+ if chat_id is None:
+ return set()
+ if isinstance(chat_id, int):
+ return {chat_id}
+ return set(chat_id)
+
+ @staticmethod
+ def _parse_username(username: SLT[str]) -> Set[str]:
+ if username is None:
+ return set()
+ if isinstance(username, str):
+ return {username[1:] if username.startswith('@') else username}
+ return {chat[1:] if chat.startswith('@') else chat for chat in username}
+
+ def _set_chat_ids(self, chat_id: SLT[int]) -> None:
+ with self.__lock:
+ if chat_id and self._usernames:
+ raise RuntimeError(
+ f"Can't set {self.chat_id_name} in conjunction with (already set) "
+ f"{self.username_name}s."
+ )
+ self._chat_ids = self._parse_chat_id(chat_id)
+
+ def _set_usernames(self, username: SLT[str]) -> None:
+ with self.__lock:
+ if username and self._chat_ids:
+ raise RuntimeError(
+ f"Can't set {self.username_name} in conjunction with (already set) "
+ f"{self.chat_id_name}s."
+ )
+ self._usernames = self._parse_username(username)
+
+ @property
+ def chat_ids(self) -> FrozenSet[int]:
+ with self.__lock:
+ return frozenset(self._chat_ids)
+
+ @chat_ids.setter
+ def chat_ids(self, chat_id: SLT[int]) -> None:
+ self._set_chat_ids(chat_id)
+
+ @property
+ def usernames(self) -> FrozenSet[str]:
+ with self.__lock:
+ return frozenset(self._usernames)
+
+ @usernames.setter
+ def usernames(self, username: SLT[str]) -> None:
+ self._set_usernames(username)
+
+ def add_usernames(self, username: SLT[str]) -> None:
+ with self.__lock:
+ if self._chat_ids:
+ raise RuntimeError(
+ f"Can't set {self.username_name} in conjunction with (already set) "
+ f"{self.chat_id_name}s."
+ )
+
+ parsed_username = self._parse_username(username)
+ self._usernames |= parsed_username
+
+ def add_chat_ids(self, chat_id: SLT[int]) -> None:
+ with self.__lock:
+ if self._usernames:
+ raise RuntimeError(
+ f"Can't set {self.chat_id_name} in conjunction with (already set) "
+ f"{self.username_name}s."
+ )
+
+ parsed_chat_id = self._parse_chat_id(chat_id)
+
+ self._chat_ids |= parsed_chat_id
+
+ def remove_usernames(self, username: SLT[str]) -> None:
+ with self.__lock:
+ if self._chat_ids:
+ raise RuntimeError(
+ f"Can't set {self.username_name} in conjunction with (already set) "
+ f"{self.chat_id_name}s."
+ )
+
+ parsed_username = self._parse_username(username)
+ self._usernames -= parsed_username
+
+ def remove_chat_ids(self, chat_id: SLT[int]) -> None:
+ with self.__lock:
+ if self._usernames:
+ raise RuntimeError(
+ f"Can't set {self.chat_id_name} in conjunction with (already set) "
+ f"{self.username_name}s."
+ )
+ parsed_chat_id = self._parse_chat_id(chat_id)
+ self._chat_ids -= parsed_chat_id
+
+ def filter(self, message: Message) -> bool:
+ """""" # remove method from docs
+ chat_or_user = self.get_chat_or_user(message)
+ if chat_or_user:
+ if self.chat_ids:
+ return chat_or_user.id in self.chat_ids
+ if self.usernames:
+ return bool(chat_or_user.username and chat_or_user.username in self.usernames)
+ return self.allow_empty
+ return False
+
+ @property
+ def name(self) -> str:
+ return (
+ f'Filters.{self.__class__.__name__}('
+ f'{", ".join(str(s) for s in (self.usernames or self.chat_ids))})'
+ )
+
+ @name.setter
+ def name(self, name: str) -> NoReturn:
+ raise RuntimeError(f'Cannot set name for Filters.{self.__class__.__name__}')
+
+ class user(_ChatUserBaseFilter):
+ # pylint: disable=W0235
+ """Filters messages to allow only those which are from specified user ID(s) or
+ username(s).
Examples:
``MessageHandler(Filters.user(1234), callback_method)``
+ Warning:
+ :attr:`user_ids` will give a *copy* of the saved user ids as :class:`frozenset`. This
+ is to ensure thread safety. To add/remove a user, you should use :meth:`add_usernames`,
+ :meth:`add_user_ids`, :meth:`remove_usernames` and :meth:`remove_user_ids`. Only update
+ the entire set by ``filter.user_ids/usernames = new_set``, if you are entirely sure
+ that it is not causing race conditions, as this will complete replace the current set
+ of allowed users.
+
Args:
- user_id(:obj:`int` | List[:obj:`int`], optional): Which user ID(s) to allow through.
- username(:obj:`str` | List[:obj:`str`], optional): Which username(s) to allow through.
- If username starts with '@' symbol, it will be ignored.
+ user_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which user ID(s) to allow through.
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to allow through. Leading ``'@'`` s in usernames will be
+ discarded.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user
+ is specified in :attr:`user_ids` and :attr:`usernames`. Defaults to :obj:`False`
Raises:
- ValueError: If chat_id and username are both present, or neither is.
+ RuntimeError: If user_id and username are both present.
+
+ Attributes:
+ user_ids(set(:obj:`int`), optional): Which user ID(s) to allow through.
+ usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to
+ allow through.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user
+ is specified in :attr:`user_ids` and :attr:`usernames`.
"""
- def __init__(self, user_id=None, username=None):
- if not (bool(user_id) ^ bool(username)):
- raise ValueError('One and only one of user_id or username must be used')
- if user_id is not None and isinstance(user_id, int):
- self.user_ids = [user_id]
- else:
- self.user_ids = user_id
- if username is None:
- self.usernames = username
- elif isinstance(username, string_types):
- self.usernames = [username.replace('@', '')]
- else:
- self.usernames = [user.replace('@', '') for user in username]
+ __slots__ = ()
- def filter(self, message):
- if self.user_ids is not None:
- return bool(message.from_user and message.from_user.id in self.user_ids)
- else:
- # self.usernames is not None
- return bool(message.from_user and message.from_user.username and
- message.from_user.username in self.usernames)
+ def __init__(
+ self,
+ user_id: SLT[int] = None,
+ username: SLT[str] = None,
+ allow_empty: bool = False,
+ ):
+ super().__init__(chat_id=user_id, username=username, allow_empty=allow_empty)
+ self.chat_id_name = 'user_id'
- class chat(BaseFilter):
- """Filters messages to allow only those which are from specified chat ID.
+ def get_chat_or_user(self, message: Message) -> Optional[User]:
+ return message.from_user
+
+ @property
+ def user_ids(self) -> FrozenSet[int]:
+ return self.chat_ids
+
+ @user_ids.setter
+ def user_ids(self, user_id: SLT[int]) -> None:
+ self.chat_ids = user_id # type: ignore[assignment]
+
+ def add_usernames(self, username: SLT[str]) -> None:
+ """
+ Add one or more users to the allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to allow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().add_usernames(username)
+
+ def add_user_ids(self, user_id: SLT[int]) -> None:
+ """
+ Add one or more users to the allowed user ids.
+
+ Args:
+ user_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which user ID(s) to allow through.
+ """
+ return super().add_chat_ids(user_id)
+
+ def remove_usernames(self, username: SLT[str]) -> None:
+ """
+ Remove one or more users from allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to disallow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().remove_usernames(username)
+
+ def remove_user_ids(self, user_id: SLT[int]) -> None:
+ """
+ Remove one or more users from allowed user ids.
+
+ Args:
+ user_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which user ID(s) to disallow through.
+ """
+ return super().remove_chat_ids(user_id)
+
+ class via_bot(_ChatUserBaseFilter):
+ # pylint: disable=W0235
+ """Filters messages to allow only those which are from specified via_bot ID(s) or
+ username(s).
+
+ Examples:
+ ``MessageHandler(Filters.via_bot(1234), callback_method)``
+
+ Warning:
+ :attr:`bot_ids` will give a *copy* of the saved bot ids as :class:`frozenset`. This
+ is to ensure thread safety. To add/remove a bot, you should use :meth:`add_usernames`,
+ :meth:`add_bot_ids`, :meth:`remove_usernames` and :meth:`remove_bot_ids`. Only update
+ the entire set by ``filter.bot_ids/usernames = new_set``, if you are entirely sure
+ that it is not causing race conditions, as this will complete replace the current set
+ of allowed bots.
+
+ Args:
+ bot_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which bot ID(s) to allow through.
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to allow through. Leading ``'@'`` s in usernames will be
+ discarded.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user
+ is specified in :attr:`bot_ids` and :attr:`usernames`. Defaults to :obj:`False`
+
+ Raises:
+ RuntimeError: If bot_id and username are both present.
+
+ Attributes:
+ bot_ids(set(:obj:`int`), optional): Which bot ID(s) to allow through.
+ usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to
+ allow through.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no bot
+ is specified in :attr:`bot_ids` and :attr:`usernames`.
+
+ """
+
+ __slots__ = ()
+
+ def __init__(
+ self,
+ bot_id: SLT[int] = None,
+ username: SLT[str] = None,
+ allow_empty: bool = False,
+ ):
+ super().__init__(chat_id=bot_id, username=username, allow_empty=allow_empty)
+ self.chat_id_name = 'bot_id'
+
+ def get_chat_or_user(self, message: Message) -> Optional[User]:
+ return message.via_bot
+
+ @property
+ def bot_ids(self) -> FrozenSet[int]:
+ return self.chat_ids
+
+ @bot_ids.setter
+ def bot_ids(self, bot_id: SLT[int]) -> None:
+ self.chat_ids = bot_id # type: ignore[assignment]
+
+ def add_usernames(self, username: SLT[str]) -> None:
+ """
+ Add one or more users to the allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to allow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().add_usernames(username)
+
+ def add_bot_ids(self, bot_id: SLT[int]) -> None:
+ """
+
+ Add one or more users to the allowed user ids.
+
+ Args:
+ bot_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which bot ID(s) to allow through.
+ """
+ return super().add_chat_ids(bot_id)
+
+ def remove_usernames(self, username: SLT[str]) -> None:
+ """
+ Remove one or more users from allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to disallow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().remove_usernames(username)
+
+ def remove_bot_ids(self, bot_id: SLT[int]) -> None:
+ """
+ Remove one or more users from allowed user ids.
+
+ Args:
+ bot_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which bot ID(s) to disallow through.
+ """
+ return super().remove_chat_ids(bot_id)
+
+ class chat(_ChatUserBaseFilter):
+ # pylint: disable=W0235
+ """Filters messages to allow only those which are from a specified chat ID or username.
Examples:
``MessageHandler(Filters.chat(-1234), callback_method)``
+ Warning:
+ :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This
+ is to ensure thread safety. To add/remove a chat, you should use :meth:`add_usernames`,
+ :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update
+ the entire set by ``filter.chat_ids/usernames = new_set``, if you are entirely sure
+ that it is not causing race conditions, as this will complete replace the current set
+ of allowed chats.
+
Args:
- chat_id(:obj:`int` | List[:obj:`int`], optional): Which chat ID(s) to allow through.
- username(:obj:`str` | List[:obj:`str`], optional): Which username(s) to allow through.
- If username start swith '@' symbol, it will be ignored.
+ chat_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which chat ID(s) to allow through.
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to allow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat
+ is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`
Raises:
- ValueError: If chat_id and username are both present, or neither is.
+ RuntimeError: If chat_id and username are both present.
+
+ Attributes:
+ chat_ids(set(:obj:`int`), optional): Which chat ID(s) to allow through.
+ usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to
+ allow through.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat
+ is specified in :attr:`chat_ids` and :attr:`usernames`.
"""
- def __init__(self, chat_id=None, username=None):
- if not (bool(chat_id) ^ bool(username)):
- raise ValueError('One and only one of chat_id or username must be used')
- if chat_id is not None and isinstance(chat_id, int):
- self.chat_ids = [chat_id]
- else:
- self.chat_ids = chat_id
- if username is None:
- self.usernames = username
- elif isinstance(username, string_types):
- self.usernames = [username.replace('@', '')]
- else:
- self.usernames = [chat.replace('@', '') for chat in username]
+ __slots__ = ()
- def filter(self, message):
- if self.chat_ids is not None:
- return bool(message.chat_id in self.chat_ids)
- else:
- # self.usernames is not None
- return bool(message.chat.username and message.chat.username in self.usernames)
+ def get_chat_or_user(self, message: Message) -> Optional[Chat]:
+ return message.chat
+
+ def add_usernames(self, username: SLT[str]) -> None:
+ """
+ Add one or more chats to the allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to allow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().add_usernames(username)
+
+ def add_chat_ids(self, chat_id: SLT[int]) -> None:
+ """
+ Add one or more chats to the allowed chat ids.
+
+ Args:
+ chat_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which chat ID(s) to allow through.
+ """
+ return super().add_chat_ids(chat_id)
+
+ def remove_usernames(self, username: SLT[str]) -> None:
+ """
+ Remove one or more chats from allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to disallow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().remove_usernames(username)
+
+ def remove_chat_ids(self, chat_id: SLT[int]) -> None:
+ """
+ Remove one or more chats from allowed chat ids.
+
+ Args:
+ chat_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which chat ID(s) to disallow through.
+ """
+ return super().remove_chat_ids(chat_id)
+
+ class forwarded_from(_ChatUserBaseFilter):
+ # pylint: disable=W0235
+ """Filters messages to allow only those which are forwarded from the specified chat ID(s)
+ or username(s) based on :attr:`telegram.Message.forward_from` and
+ :attr:`telegram.Message.forward_from_chat`.
+
+ .. versionadded:: 13.5
+
+ Examples:
+ ``MessageHandler(Filters.forwarded_from(chat_id=1234), callback_method)``
+
+ Note:
+ When a user has disallowed adding a link to their account while forwarding their
+ messages, this filter will *not* work since both
+ :attr:`telegram.Message.forwarded_from` and
+ :attr:`telegram.Message.forwarded_from_chat` are :obj:`None`. However, this behaviour
+ is undocumented and might be changed by Telegram.
+
+ Warning:
+ :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This
+ is to ensure thread safety. To add/remove a chat, you should use :meth:`add_usernames`,
+ :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update
+ the entire set by ``filter.chat_ids/usernames = new_set``, if you are entirely sure
+ that it is not causing race conditions, as this will complete replace the current set
+ of allowed chats.
+
+ Args:
+ chat_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which chat/user ID(s) to allow through.
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to allow through. Leading ``'@'`` s in usernames will be
+ discarded.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat
+ is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`.
+
+ Raises:
+ RuntimeError: If both chat_id and username are present.
+
+ Attributes:
+ chat_ids(set(:obj:`int`), optional): Which chat/user ID(s) to allow through.
+ usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to
+ allow through.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat
+ is specified in :attr:`chat_ids` and :attr:`usernames`.
+ """
+
+ __slots__ = ()
+
+ def get_chat_or_user(self, message: Message) -> Union[User, Chat, None]:
+ return message.forward_from or message.forward_from_chat
+
+ def add_usernames(self, username: SLT[str]) -> None:
+ """
+ Add one or more chats to the allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to allow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().add_usernames(username)
+
+ def add_chat_ids(self, chat_id: SLT[int]) -> None:
+ """
+ Add one or more chats to the allowed chat ids.
+
+ Args:
+ chat_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which chat/user ID(s) to allow through.
+ """
+ return super().add_chat_ids(chat_id)
+
+ def remove_usernames(self, username: SLT[str]) -> None:
+ """
+ Remove one or more chats from allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which username(s) to disallow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().remove_usernames(username)
+
+ def remove_chat_ids(self, chat_id: SLT[int]) -> None:
+ """
+ Remove one or more chats from allowed chat ids.
+
+ Args:
+ chat_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which chat/user ID(s) to disallow through.
+ """
+ return super().remove_chat_ids(chat_id)
+
+ class sender_chat(_ChatUserBaseFilter):
+ # pylint: disable=W0235
+ """Filters messages to allow only those which are from a specified sender chat's chat ID or
+ username.
+
+ Examples:
+ * To filter for messages sent to a group by a channel with ID
+ ``-1234``, use ``MessageHandler(Filters.sender_chat(-1234), callback_method)``.
+ * To filter for messages of anonymous admins in a super group with username
+ ``@anonymous``, use
+ ``MessageHandler(Filters.sender_chat(username='anonymous'), callback_method)``.
+ * To filter for messages sent to a group by *any* channel, use
+ ``MessageHandler(Filters.sender_chat.channel, callback_method)``.
+ * To filter for messages of anonymous admins in *any* super group, use
+ ``MessageHandler(Filters.sender_chat.super_group, callback_method)``.
+
+ Note:
+ Remember, ``sender_chat`` is also set for messages in a channel as the channel itself,
+ so when your bot is an admin in a channel and the linked discussion group, you would
+ receive the message twice (once from inside the channel, once inside the discussion
+ group). Since v13.9, the field :attr:`telegram.Message.is_automatic_forward` will be
+ :obj:`True` for the discussion group message.
+
+ .. seealso:: :attr:`Filters.is_automatic_forward`
+
+ Warning:
+ :attr:`chat_ids` will return a *copy* of the saved chat ids as :class:`frozenset`. This
+ is to ensure thread safety. To add/remove a chat, you should use :meth:`add_usernames`,
+ :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update
+ the entire set by ``filter.chat_ids/usernames = new_set``, if you are entirely sure
+ that it is not causing race conditions, as this will complete replace the current set
+ of allowed chats.
+
+ Args:
+ chat_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which sender chat chat ID(s) to allow through.
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which sender chat username(s) to allow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender
+ chat is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to
+ :obj:`False`
+
+ Raises:
+ RuntimeError: If both chat_id and username are present.
+
+ Attributes:
+ chat_ids(set(:obj:`int`), optional): Which sender chat chat ID(s) to allow through.
+ usernames(set(:obj:`str`), optional): Which sender chat username(s) (without leading
+ ``'@'``) to allow through.
+ allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender
+ chat is specified in :attr:`chat_ids` and :attr:`usernames`.
+ super_group: Messages whose sender chat is a super group.
+
+ Examples:
+ ``Filters.sender_chat.supergroup``
+ channel: Messages whose sender chat is a channel.
+
+ Examples:
+ ``Filters.sender_chat.channel``
+
+ """
+
+ __slots__ = ()
+
+ def get_chat_or_user(self, message: Message) -> Optional[Chat]:
+ return message.sender_chat
+
+ def add_usernames(self, username: SLT[str]) -> None:
+ """
+ Add one or more sender chats to the allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which sender chat username(s) to allow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().add_usernames(username)
+
+ def add_chat_ids(self, chat_id: SLT[int]) -> None:
+ """
+ Add one or more sender chats to the allowed chat ids.
+
+ Args:
+ chat_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which sender chat ID(s) to allow through.
+ """
+ return super().add_chat_ids(chat_id)
- class _Invoice(BaseFilter):
+ def remove_usernames(self, username: SLT[str]) -> None:
+ """
+ Remove one or more sender chats from allowed usernames.
+
+ Args:
+ username(:class:`telegram.utils.types.SLT[str]`, optional):
+ Which sender chat username(s) to disallow through.
+ Leading ``'@'`` s in usernames will be discarded.
+ """
+ return super().remove_usernames(username)
+
+ def remove_chat_ids(self, chat_id: SLT[int]) -> None:
+ """
+ Remove one or more sender chats from allowed chat ids.
+
+ Args:
+ chat_id(:class:`telegram.utils.types.SLT[int]`, optional):
+ Which sender chat ID(s) to disallow through.
+ """
+ return super().remove_chat_ids(chat_id)
+
+ class _SuperGroup(MessageFilter):
+ __slots__ = ()
+
+ def filter(self, message: Message) -> bool:
+ if message.sender_chat:
+ return message.sender_chat.type == Chat.SUPERGROUP
+ return False
+
+ class _Channel(MessageFilter):
+ __slots__ = ()
+
+ def filter(self, message: Message) -> bool:
+ if message.sender_chat:
+ return message.sender_chat.type == Chat.CHANNEL
+ return False
+
+ super_group = _SuperGroup()
+ channel = _Channel()
+
+ class _IsAutomaticForward(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.is_automatic_forward'
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.is_automatic_forward)
+
+ is_automatic_forward = _IsAutomaticForward()
+ """Messages that contain :attr:`telegram.Message.is_automatic_forward`.
+
+ .. versionadded:: 13.9
+ """
+
+ class _HasProtectedContent(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.has_protected_content'
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.has_protected_content)
+
+ has_protected_content = _HasProtectedContent()
+ """Messages that contain :attr:`telegram.Message.has_protected_content`.
+
+ .. versionadded:: 13.9
+ """
+
+ class _Invoice(MessageFilter):
+ __slots__ = ()
name = 'Filters.invoice'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.invoice)
invoice = _Invoice()
- """:obj:`Filter`: Messages that contain :class:`telegram.Invoice`."""
+ """Messages that contain :class:`telegram.Invoice`."""
- class _SuccessfulPayment(BaseFilter):
+ class _SuccessfulPayment(MessageFilter):
+ __slots__ = ()
name = 'Filters.successful_payment'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.successful_payment)
successful_payment = _SuccessfulPayment()
- """:obj:`Filter`: Messages that confirm a :class:`telegram.SuccessfulPayment`."""
+ """Messages that confirm a :class:`telegram.SuccessfulPayment`."""
- class _PassportData(BaseFilter):
+ class _PassportData(MessageFilter):
+ __slots__ = ()
name = 'Filters.passport_data'
- def filter(self, message):
+ def filter(self, message: Message) -> bool:
return bool(message.passport_data)
passport_data = _PassportData()
- """:obj:`Filter`: Messages that contain a :class:`telegram.PassportData`"""
+ """Messages that contain a :class:`telegram.PassportData`"""
+
+ class _Poll(MessageFilter):
+ __slots__ = ()
+ name = 'Filters.poll'
- class language(BaseFilter):
+ def filter(self, message: Message) -> bool:
+ return bool(message.poll)
+
+ poll = _Poll()
+ """Messages that contain a :class:`telegram.Poll`."""
+
+ class _Dice(_DiceEmoji):
+ __slots__ = ()
+ dice = _DiceEmoji('🎲', 'dice')
+ darts = _DiceEmoji('🎯', 'darts')
+ basketball = _DiceEmoji('🏀', 'basketball')
+ football = _DiceEmoji('⚽')
+ slot_machine = _DiceEmoji('🎰')
+ bowling = _DiceEmoji('🎳', 'bowling')
+
+ dice = _Dice()
+ """Dice Messages. If an integer or a list of integers is passed, it filters messages to only
+ allow those whose dice value is appearing in the given list.
+
+ Examples:
+ To allow any dice message, simply use
+ ``MessageHandler(Filters.dice, callback_method)``.
+
+ To allow only dice messages with the emoji 🎲, but any value, use
+ ``MessageHandler(Filters.dice.dice, callback_method)``.
+
+ To allow only dice messages with the emoji 🎯 and with value 6, use
+ ``MessageHandler(Filters.dice.darts(6), callback_method)``.
+
+ To allow only dice messages with the emoji ⚽ and with value 5 `or` 6, use
+ ``MessageHandler(Filters.dice.football([5, 6]), callback_method)``.
+
+ Note:
+ Dice messages don't have text. If you want to filter either text or dice messages, use
+ ``Filters.text | Filters.dice``.
+
+ Args:
+ update (:class:`telegram.utils.types.SLT[int]`, optional):
+ Which values to allow. If not specified, will allow any dice message.
+
+ Attributes:
+ dice: Dice messages with the emoji 🎲. Passing a list of integers is supported just as for
+ :attr:`Filters.dice`.
+ darts: Dice messages with the emoji 🎯. Passing a list of integers is supported just as for
+ :attr:`Filters.dice`.
+ basketball: Dice messages with the emoji 🏀. Passing a list of integers is supported just
+ as for :attr:`Filters.dice`.
+ football: Dice messages with the emoji ⚽. Passing a list of integers is supported just
+ as for :attr:`Filters.dice`.
+ slot_machine: Dice messages with the emoji 🎰. Passing a list of integers is supported just
+ as for :attr:`Filters.dice`.
+ bowling: Dice messages with the emoji 🎳. Passing a list of integers is supported just
+ as for :attr:`Filters.dice`.
+
+ .. versionadded:: 13.4
+
+ """
+
+ class language(MessageFilter):
"""Filters messages to only allow those which are from users with a certain language code.
- Note: According to telegrams documentation, every single user does not have the
- `language_code` attribute.
+ Note:
+ According to official Telegram API documentation, not every single user has the
+ `language_code` attribute. Do not count on this filter working on all users.
Examples:
``MessageHandler(Filters.language("en"), callback_method)``
Args:
- lang (:obj:`str` | List[:obj:`str`]): Which language code(s) to allow through. This
- will be matched using ``.startswith`` meaning that 'en' will match both 'en_US'
- and 'en_GB'.
+ lang (:class:`telegram.utils.types.SLT[str]`):
+ Which language code(s) to allow through.
+ This will be matched using ``.startswith`` meaning that
+ 'en' will match both 'en_US' and 'en_GB'.
"""
- def __init__(self, lang):
- if isinstance(lang, string_types):
+ __slots__ = ('lang',)
+
+ def __init__(self, lang: SLT[str]):
+ if isinstance(lang, str):
+ lang = cast(str, lang)
self.lang = [lang]
else:
+ lang = cast(List[str], lang)
self.lang = lang
- self.name = 'Filters.language({})'.format(self.lang)
+ self.name = f'Filters.language({self.lang})'
+
+ def filter(self, message: Message) -> bool:
+ """""" # remove method from docs
+ return bool(
+ message.from_user.language_code
+ and any(message.from_user.language_code.startswith(x) for x in self.lang)
+ )
+
+ class _Attachment(MessageFilter):
+ __slots__ = ()
+
+ name = 'Filters.attachment'
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.effective_attachment)
+
+ attachment = _Attachment()
+ """Messages that contain :meth:`telegram.Message.effective_attachment`.
+
+
+ .. versionadded:: 13.6"""
+
+ class _UpdateType(UpdateFilter):
+ __slots__ = ()
+ name = 'Filters.update'
- def filter(self, message):
- return message.from_user.language_code and any(
- [message.from_user.language_code.startswith(x) for x in self.lang])
+ class _Message(UpdateFilter):
+ __slots__ = ()
+ name = 'Filters.update.message'
+
+ def filter(self, update: Update) -> bool:
+ return update.message is not None
+
+ message = _Message()
+
+ class _EditedMessage(UpdateFilter):
+ __slots__ = ()
+ name = 'Filters.update.edited_message'
+
+ def filter(self, update: Update) -> bool:
+ return update.edited_message is not None
+
+ edited_message = _EditedMessage()
+
+ class _Messages(UpdateFilter):
+ __slots__ = ()
+ name = 'Filters.update.messages'
+
+ def filter(self, update: Update) -> bool:
+ return update.message is not None or update.edited_message is not None
+
+ messages = _Messages()
+
+ class _ChannelPost(UpdateFilter):
+ __slots__ = ()
+ name = 'Filters.update.channel_post'
+
+ def filter(self, update: Update) -> bool:
+ return update.channel_post is not None
+
+ channel_post = _ChannelPost()
+
+ class _EditedChannelPost(UpdateFilter):
+ __slots__ = ()
+ name = 'Filters.update.edited_channel_post'
+
+ def filter(self, update: Update) -> bool:
+ return update.edited_channel_post is not None
+
+ edited_channel_post = _EditedChannelPost()
+
+ class _ChannelPosts(UpdateFilter):
+ __slots__ = ()
+ name = 'Filters.update.channel_posts'
+
+ def filter(self, update: Update) -> bool:
+ return update.channel_post is not None or update.edited_channel_post is not None
+
+ channel_posts = _ChannelPosts()
+
+ def filter(self, update: Update) -> bool:
+ return bool(self.messages(update) or self.channel_posts(update))
+
+ update = _UpdateType()
+ """Subset for filtering the type of update.
+
+ Examples:
+ Use these filters like: ``Filters.update.message`` or
+ ``Filters.update.channel_posts`` etc. Or use just ``Filters.update`` for all
+ types.
+
+ Attributes:
+ message: Updates with :attr:`telegram.Update.message`
+ edited_message: Updates with :attr:`telegram.Update.edited_message`
+ messages: Updates with either :attr:`telegram.Update.message` or
+ :attr:`telegram.Update.edited_message`
+ channel_post: Updates with :attr:`telegram.Update.channel_post`
+ edited_channel_post: Updates with
+ :attr:`telegram.Update.edited_channel_post`
+ channel_posts: Updates with either :attr:`telegram.Update.channel_post` or
+ :attr:`telegram.Update.edited_channel_post`
+ """
diff --git a/telegramer/include/telegram/ext/handler.py b/telegramer/include/telegram/ext/handler.py
index 5a3b6dc..b6e3a63 100644
--- a/telegramer/include/telegram/ext/handler.py
+++ b/telegramer/include/telegram/ext/handler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,21 +17,26 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the base class for handlers as used by the Dispatcher."""
+from abc import ABC, abstractmethod
+from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar, Union, Generic
+from sys import version_info as py_ver
+from telegram.utils.deprecate import set_new_attribute_deprecated
-class Handler(object):
- """The base class for all update handlers. Create custom handlers by inheriting from it.
+from telegram import Update
+from telegram.ext.utils.promise import Promise
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+from telegram.ext.utils.types import CCT
- Attributes:
- callback (:obj:`callable`): The callback function for this handler.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
- pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
- the callback function.
- pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
- the callback function.
+if TYPE_CHECKING:
+ from telegram.ext import Dispatcher
+
+RT = TypeVar('RT')
+UT = TypeVar('UT')
+
+
+class Handler(Generic[UT, CCT], ABC):
+ """The base class for all update handlers. Create custom handlers by inheriting from it.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -39,87 +44,217 @@ class Handler(object):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
Args:
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
- pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``user_data`` will be passed to the callback function. Default is ``False``.
- pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``chat_data`` will be passed to the callback function. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
"""
- def __init__(self,
- callback,
- pass_update_queue=False,
- pass_job_queue=False,
- pass_user_data=False,
- pass_chat_data=False):
+ # Apparently Py 3.7 and below have '__dict__' in ABC
+ if py_ver < (3, 7):
+ __slots__ = (
+ 'callback',
+ 'pass_update_queue',
+ 'pass_job_queue',
+ 'pass_user_data',
+ 'pass_chat_data',
+ 'run_async',
+ )
+ else:
+ __slots__ = (
+ 'callback', # type: ignore[assignment]
+ 'pass_update_queue',
+ 'pass_job_queue',
+ 'pass_user_data',
+ 'pass_chat_data',
+ 'run_async',
+ '__dict__',
+ )
+
+ def __init__(
+ self,
+ callback: Callable[[UT, CCT], RT],
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ pass_user_data: bool = False,
+ pass_chat_data: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
self.callback = callback
self.pass_update_queue = pass_update_queue
self.pass_job_queue = pass_job_queue
self.pass_user_data = pass_user_data
self.pass_chat_data = pass_chat_data
+ self.run_async = run_async
+
+ def __setattr__(self, key: str, value: object) -> None:
+ # See comment on BaseFilter to know why this was done.
+ if key.startswith('__'):
+ key = f"_{self.__class__.__name__}{key}"
+ if issubclass(self.__class__, Handler) and not self.__class__.__module__.startswith(
+ 'telegram.ext.'
+ ):
+ object.__setattr__(self, key, value)
+ return
+ set_new_attribute_deprecated(self, key, value)
- def check_update(self, update):
+ @abstractmethod
+ def check_update(self, update: object) -> Optional[Union[bool, object]]:
"""
This method is called to determine if an update should be handled by
this handler instance. It should always be overridden.
+ Note:
+ Custom updates types can be handled by the dispatcher. Therefore, an implementation of
+ this method should always check the type of :attr:`update`.
+
Args:
update (:obj:`str` | :class:`telegram.Update`): The update to be tested.
Returns:
- :obj:`bool`
+ Either :obj:`None` or :obj:`False` if the update should not be handled. Otherwise an
+ object that will be passed to :meth:`handle_update` and
+ :meth:`collect_additional_context` when the update gets handled.
"""
- raise NotImplementedError
- def handle_update(self, update, dispatcher):
+ def handle_update(
+ self,
+ update: UT,
+ dispatcher: 'Dispatcher',
+ check_result: object,
+ context: CCT = None,
+ ) -> Union[RT, Promise]:
"""
This method is called if it was determined that an update should indeed
- be handled by this instance. It should also be overridden, but in most
- cases call ``self.callback(dispatcher.bot, update)``, possibly along with
- optional arguments. To work with the ``ConversationHandler``, this method should return the
- value returned from ``self.callback``
+ be handled by this instance. Calls :attr:`callback` along with its respectful
+ arguments. To work with the :class:`telegram.ext.ConversationHandler`, this method
+ returns the value returned from :attr:`callback`.
+ Note that it can be overridden if needed by the subclassing handler.
Args:
update (:obj:`str` | :class:`telegram.Update`): The update to be handled.
- dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher to collect optional args.
+ dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher.
+ check_result (:obj:`obj`): The result from :attr:`check_update`.
+ context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by
+ the dispatcher.
"""
- raise NotImplementedError
+ run_async = self.run_async
+ if (
+ self.run_async is DEFAULT_FALSE
+ and dispatcher.bot.defaults
+ and dispatcher.bot.defaults.run_async
+ ):
+ run_async = True
+
+ if context:
+ self.collect_additional_context(context, update, dispatcher, check_result)
+ if run_async:
+ return dispatcher.run_async(self.callback, update, context, update=update)
+ return self.callback(update, context)
+
+ optional_args = self.collect_optional_args(dispatcher, update, check_result)
+ if run_async:
+ return dispatcher.run_async(
+ self.callback, dispatcher.bot, update, update=update, **optional_args
+ )
+ return self.callback(dispatcher.bot, update, **optional_args) # type: ignore
- def collect_optional_args(self, dispatcher, update=None):
- """Prepares the optional arguments that are the same for all types of handlers.
+ def collect_additional_context(
+ self,
+ context: CCT,
+ update: UT,
+ dispatcher: 'Dispatcher',
+ check_result: Any,
+ ) -> None:
+ """Prepares additional arguments for the context. Override if needed.
+
+ Args:
+ context (:class:`telegram.ext.CallbackContext`): The context object.
+ update (:class:`telegram.Update`): The update to gather chat/user id from.
+ dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher.
+ check_result: The result (return value) from :attr:`check_update`.
+
+ """
+
+ def collect_optional_args(
+ self,
+ dispatcher: 'Dispatcher',
+ update: UT = None,
+ check_result: Any = None, # pylint: disable=W0613
+ ) -> Dict[str, object]:
+ """
+ Prepares the optional arguments. If the handler has additional optional args,
+ it should subclass this method, but remember to call this super method.
+
+ DEPRECATED: This method is being replaced by new context based callbacks. Please see
+ https://git.io/fxJuV for more info.
Args:
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher.
+ update (:class:`telegram.Update`): The update to gather chat/user id from.
+ check_result: The result from check_update
"""
- optional_args = dict()
+ optional_args: Dict[str, object] = {}
if self.pass_update_queue:
optional_args['update_queue'] = dispatcher.update_queue
if self.pass_job_queue:
optional_args['job_queue'] = dispatcher.job_queue
- if self.pass_user_data or self.pass_chat_data:
- chat = update.effective_chat
+ if self.pass_user_data and isinstance(update, Update):
user = update.effective_user
-
- if self.pass_user_data:
- optional_args['user_data'] = dispatcher.user_data[user.id if user else None]
-
- if self.pass_chat_data:
- optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None]
+ optional_args['user_data'] = dispatcher.user_data[
+ user.id if user else None # type: ignore[index]
+ ]
+ if self.pass_chat_data and isinstance(update, Update):
+ chat = update.effective_chat
+ optional_args['chat_data'] = dispatcher.chat_data[
+ chat.id if chat else None # type: ignore[index]
+ ]
return optional_args
diff --git a/telegramer/include/telegram/ext/inlinequeryhandler.py b/telegramer/include/telegram/ext/inlinequeryhandler.py
index 157df93..de43431 100644
--- a/telegramer/include/telegram/ext/inlinequeryhandler.py
+++ b/telegramer/include/telegram/ext/inlinequeryhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,143 +16,206 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
-""" This module contains the InlineQueryHandler class """
+"""This module contains the InlineQueryHandler class."""
import re
-
-# REMREM from future.utils import string_types
-try:
- from future.utils import string_types
-except Exception as e:
- pass
-
-try:
- string_types
-except NameError:
- string_types = str
+from typing import (
+ TYPE_CHECKING,
+ Callable,
+ Dict,
+ Match,
+ Optional,
+ Pattern,
+ TypeVar,
+ Union,
+ cast,
+ List,
+)
from telegram import Update
-from telegram.utils.deprecate import deprecate
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+
from .handler import Handler
+from .utils.types import CCT
+if TYPE_CHECKING:
+ from telegram.ext import Dispatcher
-class InlineQueryHandler(Handler):
+RT = TypeVar('RT')
+
+
+class InlineQueryHandler(Handler[Update, CCT]):
"""
Handler class to handle Telegram inline queries. Optionally based on a regex. Read the
documentation of the ``re`` module for more information.
- Attributes:
- callback (:obj:`callable`): The callback function for this handler.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
- pattern (:obj:`str` | :obj:`Pattern`): Optional. Regex pattern to test
- :attr:`telegram.InlineQuery.query` against.
- pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the
- callback function.
- pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to
- the callback function.
- pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
- the callback function.
- pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
- the callback function.
-
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
can use to keep any data in will be sent to the :attr:`callback` function. Related to
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ * When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+ * :attr:`telegram.InlineQuery.chat_type` will not be set for inline queries from secret
+ chats and may not be set for inline queries coming from third-party clients. These
+ updates won't be handled, if :attr:`chat_types` is passed.
+
Args:
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
- pattern (:obj:`str` | :obj:`Pattern`, optional): Regex pattern. If not ``None``,
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pattern (:obj:`str` | :obj:`Pattern`, optional): Regex pattern. If not :obj:`None`,
``re.match`` is used on :attr:`telegram.InlineQuery.query` to determine if an update
should be handled by this handler.
+ chat_types (List[:obj:`str`], optional): List of allowed chat types. If passed, will only
+ handle inline queries with the appropriate :attr:`telegram.InlineQuery.chat_type`.
+
+ .. versionadded:: 13.5
pass_groups (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groups()`` as a keyword argument called ``groups``.
- Default is ``False``
+ Default is :obj:`False`
+ DEPRECATED: Please switch to context based callbacks.
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
- Default is ``False``
- pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``user_data`` will be passed to the callback function. Default is ``False``.
- pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``chat_data`` will be passed to the callback function. Default is ``False``.
+ Default is :obj:`False`
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pattern (:obj:`str` | :obj:`Pattern`): Optional. Regex pattern to test
+ :attr:`telegram.InlineQuery.query` against.
+ chat_types (List[:obj:`str`], optional): List of allowed chat types.
+
+ .. versionadded:: 13.5
+ pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the
+ callback function.
+ pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+
"""
- def __init__(self,
- callback,
- pass_update_queue=False,
- pass_job_queue=False,
- pattern=None,
- pass_groups=False,
- pass_groupdict=False,
- pass_user_data=False,
- pass_chat_data=False):
- super(InlineQueryHandler, self).__init__(
+ __slots__ = ('pattern', 'chat_types', 'pass_groups', 'pass_groupdict')
+
+ def __init__(
+ self,
+ callback: Callable[[Update, CCT], RT],
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ pattern: Union[str, Pattern] = None,
+ pass_groups: bool = False,
+ pass_groupdict: bool = False,
+ pass_user_data: bool = False,
+ pass_chat_data: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ chat_types: List[str] = None,
+ ):
+ super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
- pass_chat_data=pass_chat_data)
+ pass_chat_data=pass_chat_data,
+ run_async=run_async,
+ )
- if isinstance(pattern, string_types):
+ if isinstance(pattern, str):
pattern = re.compile(pattern)
self.pattern = pattern
+ self.chat_types = chat_types
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
- def check_update(self, update):
+ def check_update(self, update: object) -> Optional[Union[bool, Match]]:
"""
Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:class:`telegram.Update`): Incoming telegram update.
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
- """
+ """
if isinstance(update, Update) and update.inline_query:
+ if (self.chat_types is not None) and (
+ update.inline_query.chat_type not in self.chat_types
+ ):
+ return False
if self.pattern:
if update.inline_query.query:
match = re.match(self.pattern, update.inline_query.query)
- return bool(match)
+ if match:
+ return match
else:
return True
-
- def handle_update(self, update, dispatcher):
- """
- Send the update to the :attr:`callback`.
-
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
+ return None
+
+ def collect_optional_args(
+ self,
+ dispatcher: 'Dispatcher',
+ update: Update = None,
+ check_result: Optional[Union[bool, Match]] = None,
+ ) -> Dict[str, object]:
+ """Pass the results of ``re.match(pattern, query).{groups(), groupdict()}`` to the
+ callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if
+ needed.
"""
-
- optional_args = self.collect_optional_args(dispatcher, update)
+ optional_args = super().collect_optional_args(dispatcher, update, check_result)
if self.pattern:
- match = re.match(self.pattern, update.inline_query.query)
-
+ check_result = cast(Match, check_result)
if self.pass_groups:
- optional_args['groups'] = match.groups()
+ optional_args['groups'] = check_result.groups()
if self.pass_groupdict:
- optional_args['groupdict'] = match.groupdict()
-
- return self.callback(dispatcher.bot, update, **optional_args)
-
- # old non-PEP8 Handler methods
- m = "telegram.InlineQueryHandler."
- checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
- handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")
+ optional_args['groupdict'] = check_result.groupdict()
+ return optional_args
+
+ def collect_additional_context(
+ self,
+ context: CCT,
+ update: Update,
+ dispatcher: 'Dispatcher',
+ check_result: Optional[Union[bool, Match]],
+ ) -> None:
+ """Add the result of ``re.match(pattern, update.inline_query.query)`` to
+ :attr:`CallbackContext.matches` as list with one element.
+ """
+ if self.pattern:
+ check_result = cast(Match, check_result)
+ context.matches = [check_result]
diff --git a/telegramer/include/telegram/ext/jobqueue.py b/telegramer/include/telegram/ext/jobqueue.py
index 1ce8490..f0c1fba 100644
--- a/telegramer/include/telegram/ext/jobqueue.py
+++ b/telegramer/include/telegram/ext/jobqueue.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,81 +18,145 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes JobQueue and Job."""
-import logging
-import time
import datetime
-import weakref
-from numbers import Number
-from threading import Thread, Lock, Event
-# REMREM from queue import PriorityQueue, Empty
-from Queue import PriorityQueue, Empty
+import logging
+from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union, cast, overload
+import pytz
+from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED, JobEvent
+from apscheduler.schedulers.background import BackgroundScheduler
+from apscheduler.triggers.combining import OrTrigger
+from apscheduler.triggers.cron import CronTrigger
+from apscheduler.job import Job as APSJob
-class Days(object):
- MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
- EVERY_DAY = tuple(range(7))
+from telegram.ext.callbackcontext import CallbackContext
+from telegram.utils.types import JSONDict
+from telegram.utils.deprecate import set_new_attribute_deprecated
+if TYPE_CHECKING:
+ from telegram import Bot
+ from telegram.ext import Dispatcher
+ import apscheduler.job # noqa: F401
-class JobQueue(object):
- """This class allows you to periodically perform tasks with the bot.
- Attributes:
- _queue (:obj:`PriorityQueue`): The queue that holds the Jobs.
- bot (:class:`telegram.Bot`): Bot that's send to the handlers.
+class JobQueue:
+ """This class allows you to periodically perform tasks with the bot. It is a convenience
+ wrapper for the APScheduler library.
- Args:
+ Attributes:
+ scheduler (:class:`apscheduler.schedulers.background.BackgroundScheduler`): The APScheduler
bot (:class:`telegram.Bot`): The bot instance that should be passed to the jobs.
+ DEPRECATED: Use :attr:`set_dispatcher` instead.
"""
- def __init__(self, bot):
- self._queue = PriorityQueue()
- self.bot = bot
- self.logger = logging.getLogger(self.__class__.__name__)
- self.__start_lock = Lock()
- self.__next_peek_lock = Lock() # to protect self._next_peek & self.__tick
- self.__tick = Event()
- self.__thread = None
- self._next_peek = None
- self._running = False
-
- def _put(self, job, next_t=None, last_t=None):
- if next_t is None:
- next_t = job.interval
- if next_t is None:
- raise ValueError('next_t is None')
-
- if isinstance(next_t, datetime.datetime):
- next_t = (next_t - datetime.datetime.now()).total_seconds()
-
- elif isinstance(next_t, datetime.time):
- next_datetime = datetime.datetime.combine(datetime.date.today(), next_t)
-
- if datetime.datetime.now().time() > next_t:
- next_datetime += datetime.timedelta(days=1)
-
- next_t = (next_datetime - datetime.datetime.now()).total_seconds()
-
- elif isinstance(next_t, datetime.timedelta):
- next_t = next_t.total_seconds()
-
- next_t += last_t or time.time()
-
- self.logger.debug('Putting job %s with t=%f', job.name, next_t)
+ __slots__ = ('_dispatcher', 'logger', 'scheduler', '__dict__')
- self._queue.put((next_t, job))
+ def __init__(self) -> None:
+ self._dispatcher: 'Dispatcher' = None # type: ignore[assignment]
+ self.logger = logging.getLogger(self.__class__.__name__)
+ self.scheduler = BackgroundScheduler(timezone=pytz.utc)
+ self.scheduler.add_listener(
+ self._update_persistence, mask=EVENT_JOB_EXECUTED | EVENT_JOB_ERROR
+ )
+
+ # Dispatch errors and don't log them in the APS logger
+ def aps_log_filter(record): # type: ignore
+ return 'raised an exception' not in record.msg
+
+ logging.getLogger('apscheduler.executors.default').addFilter(aps_log_filter)
+ self.scheduler.add_listener(self._dispatch_error, EVENT_JOB_ERROR)
+
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
+
+ def _build_args(self, job: 'Job') -> List[Union[CallbackContext, 'Bot', 'Job']]:
+ if self._dispatcher.use_context:
+ return [self._dispatcher.context_types.context.from_job(job, self._dispatcher)]
+ return [self._dispatcher.bot, job]
+
+ def _tz_now(self) -> datetime.datetime:
+ return datetime.datetime.now(self.scheduler.timezone)
+
+ def _update_persistence(self, _: JobEvent) -> None:
+ self._dispatcher.update_persistence()
+
+ def _dispatch_error(self, event: JobEvent) -> None:
+ try:
+ self._dispatcher.dispatch_error(None, event.exception)
+ # Errors should not stop the thread.
+ except Exception:
+ self.logger.exception(
+ 'An error was raised while processing the job and an '
+ 'uncaught error was raised while handling the error '
+ 'with an error_handler.'
+ )
+
+ @overload
+ def _parse_time_input(self, time: None, shift_day: bool = False) -> None:
+ ...
+
+ @overload
+ def _parse_time_input(
+ self,
+ time: Union[float, int, datetime.timedelta, datetime.datetime, datetime.time],
+ shift_day: bool = False,
+ ) -> datetime.datetime:
+ ...
+
+ def _parse_time_input(
+ self,
+ time: Union[float, int, datetime.timedelta, datetime.datetime, datetime.time, None],
+ shift_day: bool = False,
+ ) -> Optional[datetime.datetime]:
+ if time is None:
+ return None
+ if isinstance(time, (int, float)):
+ return self._tz_now() + datetime.timedelta(seconds=time)
+ if isinstance(time, datetime.timedelta):
+ return self._tz_now() + time
+ if isinstance(time, datetime.time):
+ date_time = datetime.datetime.combine(
+ datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time
+ )
+ if date_time.tzinfo is None:
+ date_time = self.scheduler.timezone.localize(date_time)
+ if shift_day and date_time <= datetime.datetime.now(pytz.utc):
+ date_time += datetime.timedelta(days=1)
+ return date_time
+ # isinstance(time, datetime.datetime):
+ return time
+
+ def set_dispatcher(self, dispatcher: 'Dispatcher') -> None:
+ """Set the dispatcher to be used by this JobQueue. Use this instead of passing a
+ :class:`telegram.Bot` to the JobQueue, which is deprecated.
- # Wake up the loop if this job should be executed next
- self._set_next_peek(next_t)
+ Args:
+ dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher.
- def run_once(self, callback, when, context=None, name=None):
+ """
+ self._dispatcher = dispatcher
+ if dispatcher.bot.defaults:
+ self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc)
+
+ def run_once(
+ self,
+ callback: Callable[['CallbackContext'], None],
+ when: Union[float, datetime.timedelta, datetime.datetime, datetime.time],
+ context: object = None,
+ name: str = None,
+ job_kwargs: JSONDict = None,
+ ) -> 'Job':
"""Creates a new ``Job`` that runs once and adds it to the queue.
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
- job. It should take ``bot, job`` as parameters, where ``job`` is the
- :class:`telegram.ext.Job` instance. It can be used to access it's
- ``job.context`` or change it to a repeating job.
+ job. Callback signature for context based API:
+
+ ``def callback(CallbackContext)``
+
+ ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
+ its ``job.context`` or change it to a repeating job.
when (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
:obj:`datetime.datetime` | :obj:`datetime.time`):
Time in or at which the job should run. This parameter will be interpreted
@@ -103,33 +167,71 @@ def run_once(self, callback, when, context=None, name=None):
* :obj:`datetime.timedelta` will be interpreted as "time from now" in which the
job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
- which the job should run.
+ which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
+ the default timezone of the bot will be used.
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
- tomorrow.
+ tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
+ default timezone of the bot will be used.
context (:obj:`object`, optional): Additional data needed for the callback function.
- Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
+ Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
name (:obj:`str`, optional): The name of the new job. Defaults to
``callback.__name__``.
+ job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
+ ``scheduler.add_job()``.
Returns:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
"""
- job = Job(callback, repeat=False, context=context, name=name, job_queue=self)
- self._put(job, next_t=when)
+ if not job_kwargs:
+ job_kwargs = {}
+
+ name = name or callback.__name__
+ job = Job(callback, context, name, self)
+ date_time = self._parse_time_input(when, shift_day=True)
+
+ j = self.scheduler.add_job(
+ callback,
+ name=name,
+ trigger='date',
+ run_date=date_time,
+ args=self._build_args(job),
+ timezone=date_time.tzinfo or self.scheduler.timezone,
+ **job_kwargs,
+ )
+
+ job.job = j
return job
- def run_repeating(self, callback, interval, first=None, context=None, name=None):
- """Creates a new ``Job`` that runs once and adds it to the queue.
+ def run_repeating(
+ self,
+ callback: Callable[['CallbackContext'], None],
+ interval: Union[float, datetime.timedelta],
+ first: Union[float, datetime.timedelta, datetime.datetime, datetime.time] = None,
+ last: Union[float, datetime.timedelta, datetime.datetime, datetime.time] = None,
+ context: object = None,
+ name: str = None,
+ job_kwargs: JSONDict = None,
+ ) -> 'Job':
+ """Creates a new ``Job`` that runs at specified intervals and adds it to the queue.
+
+ Note:
+ For a note about DST, please see the documentation of `APScheduler`_.
+
+ .. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
+ #daylight-saving-time-behavior
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
- job. It should take ``bot, job`` as parameters, where ``job`` is the
- :class:`telegram.ext.Job` instance. It can be used to access it's
- ``Job.context`` or change it to a repeating job.
+ job. Callback signature for context based API:
+
+ ``def callback(CallbackContext)``
+
+ ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
+ its ``job.context`` or change it to a repeating job.
interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`): The interval in which
the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted
as seconds.
@@ -143,331 +245,415 @@ def run_repeating(self, callback, interval, first=None, context=None, name=None)
* :obj:`datetime.timedelta` will be interpreted as "time from now" in which the
job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
- which the job should run.
+ which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
+ the default timezone of the bot will be used.
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
- tomorrow.
+ tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
+ default timezone of the bot will be used.
Defaults to ``interval``
+ last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
+ :obj:`datetime.datetime` | :obj:`datetime.time`, optional):
+ Latest possible time for the job to run. This parameter will be interpreted
+ depending on its type. See ``first`` for details.
+
+ If ``last`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
+ and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be
+ assumed.
+
+ Defaults to :obj:`None`.
context (:obj:`object`, optional): Additional data needed for the callback function.
- Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
+ Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
name (:obj:`str`, optional): The name of the new job. Defaults to
``callback.__name__``.
+ job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
+ ``scheduler.add_job()``.
Returns:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
"""
- job = Job(callback,
- interval=interval,
- repeat=True,
- context=context,
- name=name,
- job_queue=self)
- self._put(job, next_t=first)
+ if not job_kwargs:
+ job_kwargs = {}
+
+ name = name or callback.__name__
+ job = Job(callback, context, name, self)
+
+ dt_first = self._parse_time_input(first)
+ dt_last = self._parse_time_input(last)
+
+ if dt_last and dt_first and dt_last < dt_first:
+ raise ValueError("'last' must not be before 'first'!")
+
+ if isinstance(interval, datetime.timedelta):
+ interval = interval.total_seconds()
+
+ j = self.scheduler.add_job(
+ callback,
+ trigger='interval',
+ args=self._build_args(job),
+ start_date=dt_first,
+ end_date=dt_last,
+ seconds=interval,
+ name=name,
+ **job_kwargs,
+ )
+
+ job.job = j
return job
- def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None):
- """Creates a new ``Job`` that runs once and adds it to the queue.
+ def run_monthly(
+ self,
+ callback: Callable[['CallbackContext'], None],
+ when: datetime.time,
+ day: int,
+ context: object = None,
+ name: str = None,
+ day_is_strict: bool = True,
+ job_kwargs: JSONDict = None,
+ ) -> 'Job':
+ """Creates a new ``Job`` that runs on a monthly basis and adds it to the queue.
Args:
- callback (:obj:`callable`): The callback function that should be executed by the new
- job. It should take ``bot, job`` as parameters, where ``job`` is the
- :class:`telegram.ext.Job` instance. It can be used to access it's ``Job.context``
- or change it to a repeating job.
- time (:obj:`datetime.time`): Time of day at which the job should run.
- days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
- run. Defaults to ``EVERY_DAY``
+ callback (:obj:`callable`): The callback function that should be executed by the new
+ job. Callback signature for context based API:
+
+ ``def callback(CallbackContext)``
+
+ ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
+ its ``job.context`` or change it to a repeating job.
+ when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
+ (``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
+ day (:obj:`int`): Defines the day of the month whereby the job would run. It should
+ be within the range of 1 and 31, inclusive.
context (:obj:`object`, optional): Additional data needed for the callback function.
- Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
+ Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
name (:obj:`str`, optional): The name of the new job. Defaults to
``callback.__name__``.
+ day_is_strict (:obj:`bool`, optional): If :obj:`False` and day > month.days, will pick
+ the last day in the month. Defaults to :obj:`True`.
+ job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
+ ``scheduler.add_job()``.
Returns:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
"""
- job = Job(callback,
- interval=datetime.timedelta(days=1),
- repeat=True,
- days=days,
- context=context,
- name=name,
- job_queue=self)
- self._put(job, next_t=time)
+ if not job_kwargs:
+ job_kwargs = {}
+
+ name = name or callback.__name__
+ job = Job(callback, context, name, self)
+
+ if day_is_strict:
+ j = self.scheduler.add_job(
+ callback,
+ trigger='cron',
+ args=self._build_args(job),
+ name=name,
+ day=day,
+ hour=when.hour,
+ minute=when.minute,
+ second=when.second,
+ timezone=when.tzinfo or self.scheduler.timezone,
+ **job_kwargs,
+ )
+ else:
+ trigger = OrTrigger(
+ [
+ CronTrigger(
+ day=day,
+ hour=when.hour,
+ minute=when.minute,
+ second=when.second,
+ timezone=when.tzinfo,
+ **job_kwargs,
+ ),
+ CronTrigger(
+ day='last',
+ hour=when.hour,
+ minute=when.minute,
+ second=when.second,
+ timezone=when.tzinfo or self.scheduler.timezone,
+ **job_kwargs,
+ ),
+ ]
+ )
+ j = self.scheduler.add_job(
+ callback, trigger=trigger, args=self._build_args(job), name=name, **job_kwargs
+ )
+
+ job.job = j
return job
- def _set_next_peek(self, t):
- # """
- # Set next peek if not defined or `t` is before next peek.
- # In case the next peek was set, also trigger the `self.__tick` event.
- # """
- with self.__next_peek_lock:
- if not self._next_peek or self._next_peek > t:
- self._next_peek = t
- self.__tick.set()
+ def run_daily(
+ self,
+ callback: Callable[['CallbackContext'], None],
+ time: datetime.time,
+ days: Tuple[int, ...] = tuple(range(7)),
+ context: object = None,
+ name: str = None,
+ job_kwargs: JSONDict = None,
+ ) -> 'Job':
+ """Creates a new ``Job`` that runs on a daily basis and adds it to the queue.
- def tick(self):
- """Run all jobs that are due and re-enqueue them with their interval."""
- now = time.time()
+ Note:
+ For a note about DST, please see the documentation of `APScheduler`_.
- self.logger.debug('Ticking jobs with t=%f', now)
+ .. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
+ #daylight-saving-time-behavior
- while True:
- try:
- t, job = self._queue.get(False)
- except Empty:
- break
-
- self.logger.debug('Peeked at %s with t=%f', job.name, t)
-
- if t > now:
- # We can get here in two conditions:
- # 1. At the second or later pass of the while loop, after we've already
- # processed the job(s) we were supposed to at this time.
- # 2. At the first iteration of the loop only if `self.put()` had triggered
- # `self.__tick` because `self._next_peek` wasn't set
- self.logger.debug("Next task isn't due yet. Finished!")
- self._queue.put((t, job))
- self._set_next_peek(t)
- break
-
- if job.removed:
- self.logger.debug('Removing job %s', job.name)
- continue
-
- if job.enabled:
- try:
- current_week_day = datetime.datetime.now().weekday()
- if any(day == current_week_day for day in job.days):
- self.logger.debug('Running job %s', job.name)
- job.run(self.bot)
-
- except Exception:
- self.logger.exception('An uncaught error was raised while executing job %s',
- job.name)
- else:
- self.logger.debug('Skipping disabled job %s', job.name)
+ Args:
+ callback (:obj:`callable`): The callback function that should be executed by the new
+ job. Callback signature for context based API:
- if job.repeat and not job.removed:
- self._put(job, last_t=t)
- else:
- self.logger.debug('Dropping non-repeating or removed job %s', job.name)
+ ``def callback(CallbackContext)``
- def start(self):
- """Starts the job_queue thread."""
- self.__start_lock.acquire()
-
- if not self._running:
- self._running = True
- self.__start_lock.release()
- self.__thread = Thread(target=self._main_loop, name="job_queue")
- self.__thread.start()
- self.logger.debug('%s thread started', self.__class__.__name__)
- else:
- self.__start_lock.release()
+ ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
+ its ``job.context`` or change it to a repeating job.
+ time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
+ (``time.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
+ days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
+ run (where ``0-6`` correspond to monday - sunday). Defaults to ``EVERY_DAY``
+ context (:obj:`object`, optional): Additional data needed for the callback function.
+ Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
+ name (:obj:`str`, optional): The name of the new job. Defaults to
+ ``callback.__name__``.
+ job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
+ ``scheduler.add_job()``.
- def _main_loop(self):
- """
- Thread target of thread ``job_queue``. Runs in background and performs ticks on the job
- queue.
+ Returns:
+ :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
+ queue.
"""
- while self._running:
- # self._next_peek may be (re)scheduled during self.tick() or self.put()
- with self.__next_peek_lock:
- tmout = self._next_peek - time.time() if self._next_peek else None
- self._next_peek = None
- self.__tick.clear()
+ if not job_kwargs:
+ job_kwargs = {}
+
+ name = name or callback.__name__
+ job = Job(callback, context, name, self)
+
+ j = self.scheduler.add_job(
+ callback,
+ name=name,
+ args=self._build_args(job),
+ trigger='cron',
+ day_of_week=','.join([str(d) for d in days]),
+ hour=time.hour,
+ minute=time.minute,
+ second=time.second,
+ timezone=time.tzinfo or self.scheduler.timezone,
+ **job_kwargs,
+ )
+
+ job.job = j
+ return job
- self.__tick.wait(tmout)
+ def run_custom(
+ self,
+ callback: Callable[['CallbackContext'], None],
+ job_kwargs: JSONDict,
+ context: object = None,
+ name: str = None,
+ ) -> 'Job':
+ """Creates a new customly defined ``Job``.
- # If we were woken up by self.stop(), just bail out
- if not self._running:
- break
+ Args:
+ callback (:obj:`callable`): The callback function that should be executed by the new
+ job. Callback signature for context based API:
- self.tick()
+ ``def callback(CallbackContext)``
- self.logger.debug('%s thread stopped', self.__class__.__name__)
+ ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
+ its ``job.context`` or change it to a repeating job.
+ job_kwargs (:obj:`dict`): Arbitrary keyword arguments. Used as arguments for
+ ``scheduler.add_job``.
+ context (:obj:`object`, optional): Additional data needed for the callback function.
+ Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
+ name (:obj:`str`, optional): The name of the new job. Defaults to
+ ``callback.__name__``.
- def stop(self):
- """Stops the thread."""
- with self.__start_lock:
- self._running = False
+ Returns:
+ :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
+ queue.
- self.__tick.set()
- if self.__thread is not None:
- self.__thread.join()
+ """
+ name = name or callback.__name__
+ job = Job(callback, context, name, self)
- def jobs(self):
- """Returns a tuple of all jobs that are currently in the ``JobQueue``."""
- with self._queue.mutex:
- return tuple(job[1] for job in self._queue.queue if job)
+ j = self.scheduler.add_job(callback, args=self._build_args(job), name=name, **job_kwargs)
- def get_jobs_by_name(self, name):
- """Returns a tuple of jobs with the given name that are currently in the ``JobQueue``"""
- with self._queue.mutex:
- return tuple(job[1] for job in self._queue.queue if job and job[1].name == name)
+ job.job = j
+ return job
+ def start(self) -> None:
+ """Starts the job_queue thread."""
+ if not self.scheduler.running:
+ self.scheduler.start()
-class Job(object):
- """This class encapsulates a Job.
+ def stop(self) -> None:
+ """Stops the thread."""
+ if self.scheduler.running:
+ self.scheduler.shutdown()
+
+ def jobs(self) -> Tuple['Job', ...]:
+ """Returns a tuple of all *scheduled* jobs that are currently in the ``JobQueue``."""
+ return tuple(
+ Job._from_aps_job(job, self) # pylint: disable=W0212
+ for job in self.scheduler.get_jobs()
+ )
+
+ def get_jobs_by_name(self, name: str) -> Tuple['Job', ...]:
+ """Returns a tuple of all *pending/scheduled* jobs with the given name that are currently
+ in the ``JobQueue``.
+ """
+ return tuple(job for job in self.jobs() if job.name == name)
- Attributes:
- callback (:obj:`callable`): The callback function that should be executed by the new job.
- context (:obj:`object`): Optional. Additional data needed for the callback function.
- name (:obj:`str`): Optional. The name of the new job.
+
+class Job:
+ """This class is a convenience wrapper for the jobs held in a :class:`telegram.ext.JobQueue`.
+ With the current backend APScheduler, :attr:`job` holds a :class:`apscheduler.job.Job`
+ instance.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
+
+ Note:
+ * All attributes and instance methods of :attr:`job` are also directly available as
+ attributes/methods of the corresponding :class:`telegram.ext.Job` object.
+ * Two instances of :class:`telegram.ext.Job` are considered equal, if their corresponding
+ ``job`` attributes have the same ``id``.
+ * If :attr:`job` isn't passed on initialization, it must be set manually afterwards for
+ this :class:`telegram.ext.Job` to be useful.
Args:
callback (:obj:`callable`): The callback function that should be executed by the new job.
- It should take ``bot, job`` as parameters, where ``job`` is the
- :class:`telegram.ext.Job` instance. It can be used to access it's :attr:`context`
- or change it to a repeating job.
- interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`, optional): The interval in
- which the job will run. If it is an :obj:`int` or a :obj:`float`, it will be
- interpreted as seconds. If you don't set this value, you must set :attr:`repeat` to
- ``False`` and specify :attr:`next_t` when you put the job into the job queue.
- repeat (:obj:`bool`, optional): If this job should be periodically execute its callback
- function (``True``) or only once (``False``). Defaults to ``True``.
+ Callback signature for context based API:
+
+ ``def callback(CallbackContext)``
+
+ a ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
+ its ``job.context`` or change it to a repeating job.
context (:obj:`object`, optional): Additional data needed for the callback function. Can be
- accessed through ``job.context`` in the callback. Defaults to ``None``.
+ accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``.
- days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should run.
- Defaults to ``Days.EVERY_DAY``
job_queue (:class:`telegram.ext.JobQueue`, optional): The ``JobQueue`` this job belongs to.
Only optional for backward compatibility with ``JobQueue.put()``.
+ job (:class:`apscheduler.job.Job`, optional): The APS Job this job is a wrapper for.
+ Attributes:
+ callback (:obj:`callable`): The callback function that should be executed by the new job.
+ context (:obj:`object`): Optional. Additional data needed for the callback function.
+ name (:obj:`str`): Optional. The name of the new job.
+ job_queue (:class:`telegram.ext.JobQueue`): Optional. The ``JobQueue`` this job belongs to.
+ job (:class:`apscheduler.job.Job`): Optional. The APS Job this job is a wrapper for.
"""
- def __init__(self,
- callback,
- interval=None,
- repeat=True,
- context=None,
- days=Days.EVERY_DAY,
- name=None,
- job_queue=None):
+ __slots__ = (
+ 'callback',
+ 'context',
+ 'name',
+ 'job_queue',
+ '_removed',
+ '_enabled',
+ 'job',
+ '__dict__',
+ )
+
+ def __init__(
+ self,
+ callback: Callable[['CallbackContext'], None],
+ context: object = None,
+ name: str = None,
+ job_queue: JobQueue = None,
+ job: APSJob = None,
+ ):
self.callback = callback
self.context = context
self.name = name or callback.__name__
+ self.job_queue = job_queue
- self._repeat = repeat
- self._interval = None
- self.interval = interval
- self.repeat = repeat
+ self._removed = False
+ self._enabled = False
- self._days = None
- self.days = days
+ self.job = cast(APSJob, job) # skipcq: PTC-W0052
- self._job_queue = weakref.proxy(job_queue) if job_queue is not None else None
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
- self._remove = Event()
- self._enabled = Event()
- self._enabled.set()
-
- def run(self, bot):
- """Executes the callback function."""
- self.callback(bot, self)
-
- def schedule_removal(self):
+ def run(self, dispatcher: 'Dispatcher') -> None:
+ """Executes the callback function independently of the jobs schedule."""
+ try:
+ if dispatcher.use_context:
+ self.callback(dispatcher.context_types.context.from_job(self, dispatcher))
+ else:
+ self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg]
+ except Exception as exc:
+ try:
+ dispatcher.dispatch_error(None, exc)
+ # Errors should not stop the thread.
+ except Exception:
+ dispatcher.logger.exception(
+ 'An error was raised while processing the job and an '
+ 'uncaught error was raised while handling the error '
+ 'with an error_handler.'
+ )
+
+ def schedule_removal(self) -> None:
"""
Schedules this job for removal from the ``JobQueue``. It will be removed without executing
its callback function again.
-
"""
- self._remove.set()
+ self.job.remove()
+ self._removed = True
@property
- def removed(self):
+ def removed(self) -> bool:
""":obj:`bool`: Whether this job is due to be removed."""
- return self._remove.is_set()
+ return self._removed
@property
- def enabled(self):
+ def enabled(self) -> bool:
""":obj:`bool`: Whether this job is enabled."""
- return self._enabled.is_set()
+ return self._enabled
@enabled.setter
- def enabled(self, status):
+ def enabled(self, status: bool) -> None:
if status:
- self._enabled.set()
+ self.job.resume()
else:
- self._enabled.clear()
+ self.job.pause()
+ self._enabled = status
@property
- def interval(self):
+ def next_t(self) -> Optional[datetime.datetime]:
"""
- :obj:`int` | :obj:`float` | :obj:`datetime.timedelta`: Optional. The interval in which the
- job will run.
-
+ :obj:`datetime.datetime`: Datetime for the next job execution.
+ Datetime is localized according to :attr:`tzinfo`.
+ If job is removed or already ran it equals to :obj:`None`.
"""
- return self._interval
-
- @interval.setter
- def interval(self, interval):
- if interval is None and self.repeat:
- raise ValueError("The 'interval' can not be 'None' when 'repeat' is set to 'True'")
-
- if not (interval is None or isinstance(interval, (Number, datetime.timedelta))):
- raise ValueError("The 'interval' must be of type 'datetime.timedelta',"
- " 'int' or 'float'")
-
- self._interval = interval
+ return self.job.next_run_time
- @property
- def interval_seconds(self):
- """:obj:`int`: The interval for this job in seconds."""
- interval = self.interval
- if isinstance(interval, datetime.timedelta):
- return interval.total_seconds()
+ @classmethod
+ def _from_aps_job(cls, job: APSJob, job_queue: JobQueue) -> 'Job':
+ # context based callbacks
+ if len(job.args) == 1:
+ context = job.args[0].job.context
else:
- return interval
-
- @property
- def repeat(self):
- """:obj:`bool`: Optional. If this job should periodically execute its callback function."""
- return self._repeat
-
- @repeat.setter
- def repeat(self, repeat):
- if self.interval is None and repeat:
- raise ValueError("'repeat' can not be set to 'True' when no 'interval' is set")
- self._repeat = repeat
-
- @property
- def days(self):
- """Tuple[:obj:`int`]: Optional. Defines on which days of the week the job should run."""
- return self._days
+ context = job.args[1].context
+ return cls(job.func, context=context, name=job.name, job_queue=job_queue, job=job)
- @days.setter
- def days(self, days):
- if not isinstance(days, tuple):
- raise ValueError("The 'days' argument should be of type 'tuple'")
+ def __getattr__(self, item: str) -> object:
+ return getattr(self.job, item)
- if not all(isinstance(day, int) for day in days):
- raise ValueError("The elements of the 'days' argument should be of type 'int'")
-
- if not all(0 <= day <= 6 for day in days):
- raise ValueError("The elements of the 'days' argument should be from 0 up to and "
- "including 6")
-
- self._days = days
-
- @property
- def job_queue(self):
- """:class:`telegram.ext.JobQueue`: Optional. The ``JobQueue`` this job belongs to."""
- return self._job_queue
-
- @job_queue.setter
- def job_queue(self, job_queue):
- # Property setter for backward compatibility with JobQueue.put()
- if not self._job_queue:
- self._job_queue = weakref.proxy(job_queue)
- else:
- raise RuntimeError("The 'job_queue' attribute can only be set once.")
+ def __lt__(self, other: object) -> bool:
+ return False
- def __lt__(self, other):
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, self.__class__):
+ return self.id == other.id
return False
diff --git a/telegramer/include/telegram/ext/messagehandler.py b/telegramer/include/telegram/ext/messagehandler.py
index dadbee5..57faa3f 100644
--- a/telegramer/include/telegram/ext/messagehandler.py
+++ b/telegramer/include/telegram/ext/messagehandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,34 +19,24 @@
# TODO: Remove allow_edited
"""This module contains the MessageHandler class."""
import warnings
+from typing import TYPE_CHECKING, Callable, Dict, Optional, TypeVar, Union
from telegram import Update
+from telegram.ext import BaseFilter, Filters
+from telegram.utils.deprecate import TelegramDeprecationWarning
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+
from .handler import Handler
+from .utils.types import CCT
+if TYPE_CHECKING:
+ from telegram.ext import Dispatcher
-class MessageHandler(Handler):
- """Handler class to handle telegram messages. They might contain text, media or status updates.
+RT = TypeVar('RT')
- Attributes:
- filters (:obj:`Filter`): Only allow updates with these Filters. See
- :mod:`telegram.ext.filters` for a full list of all available filters.
- callback (:obj:`callable`): The callback function for this handler.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
- pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
- the callback function.
- pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
- the callback function.
- message_updates (:obj:`bool`): Optional. Should "normal" message updates be handled?
- Default is ``True``.
- channel_post_updates (:obj:`bool`): Optional. Should channel posts updates be handled?
- Default is ``True``.
- edited_updates (:obj:`bool`): Optional. Should "edited" message updates be handled?
- Default is ``False``.
- allow_edited (:obj:`bool`): Optional. If the handler should also accept edited messages.
- Default is ``False`` - Deprecated. use edited_updates instead.
+
+class MessageHandler(Handler[Update, CCT]):
+ """Handler class to handle telegram messages. They might contain text, media or status updates.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -54,116 +44,165 @@ class MessageHandler(Handler):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
Args:
filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from
:class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in
:class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise
- operators (& for and, | for or, ~ for not).
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ operators (& for and, | for or, ~ for not). Default is
+ :attr:`telegram.ext.filters.Filters.update`. This defaults to all message_type updates
+ being: ``message``, ``edited_message``, ``channel_post`` and ``edited_channel_post``.
+ If you don't want or need any of those pass ``~Filters.update.*`` in the filter
+ argument.
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
- pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``user_data`` will be passed to the callback function. Default is ``False``.
- pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``chat_data`` will be passed to the callback function. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
message_updates (:obj:`bool`, optional): Should "normal" message updates be handled?
- Default is ``True``.
+ Default is :obj:`None`.
+ DEPRECATED: Please switch to filters for update filtering.
channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled?
- Default is ``True``.
+ Default is :obj:`None`.
+ DEPRECATED: Please switch to filters for update filtering.
edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default
- is ``False``.
- allow_edited (:obj:`bool`, optional): If the handler should also accept edited messages.
- Default is ``False`` - Deprecated. use edited_updates instead.
+ is :obj:`None`.
+ DEPRECATED: Please switch to filters for update filtering.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
Raises:
ValueError
- """
+ Attributes:
+ filters (:obj:`Filter`): Only allow updates with these Filters. See
+ :mod:`telegram.ext.filters` for a full list of all available filters.
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ message_updates (:obj:`bool`): Should "normal" message updates be handled?
+ Default is :obj:`None`.
+ channel_post_updates (:obj:`bool`): Should channel posts updates be handled?
+ Default is :obj:`None`.
+ edited_updates (:obj:`bool`): Should "edited" message updates be handled?
+ Default is :obj:`None`.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
- def __init__(self,
- filters,
- callback,
- allow_edited=False,
- pass_update_queue=False,
- pass_job_queue=False,
- pass_user_data=False,
- pass_chat_data=False,
- message_updates=True,
- channel_post_updates=True,
- edited_updates=False):
- if not message_updates and not channel_post_updates and not edited_updates:
- raise ValueError(
- 'message_updates, channel_post_updates and edited_updates are all False')
- if allow_edited:
- warnings.warn('allow_edited is getting deprecated, please use edited_updates instead')
- edited_updates = allow_edited
+ """
- super(MessageHandler, self).__init__(
+ __slots__ = ('filters',)
+
+ def __init__(
+ self,
+ filters: BaseFilter,
+ callback: Callable[[Update, CCT], RT],
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ pass_user_data: bool = False,
+ pass_chat_data: bool = False,
+ message_updates: bool = None,
+ channel_post_updates: bool = None,
+ edited_updates: bool = None,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
+
+ super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
- pass_chat_data=pass_chat_data)
- self.filters = filters
- self.message_updates = message_updates
- self.channel_post_updates = channel_post_updates
- self.edited_updates = edited_updates
-
- # We put this up here instead of with the rest of checking code
- # in check_update since we don't wanna spam a ton
- if isinstance(self.filters, list):
- warnings.warn('Using a list of filters in MessageHandler is getting '
- 'deprecated, please use bitwise operators (& and |) '
- 'instead. More info: https://git.io/vPTbc.')
-
- def _is_allowed_update(self, update):
- return any([self.message_updates and update.message,
- self.edited_updates and (update.edited_message or update.edited_channel_post),
- self.channel_post_updates and update.channel_post])
-
- def check_update(self, update):
+ pass_chat_data=pass_chat_data,
+ run_async=run_async,
+ )
+ if message_updates is False and channel_post_updates is False and edited_updates is False:
+ raise ValueError(
+ 'message_updates, channel_post_updates and edited_updates are all False'
+ )
+ if filters is not None:
+ self.filters = Filters.update & filters
+ else:
+ self.filters = Filters.update
+ if message_updates is not None:
+ warnings.warn(
+ 'message_updates is deprecated. See https://git.io/fxJuV for more info',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+ if message_updates is False:
+ self.filters &= ~Filters.update.message
+
+ if channel_post_updates is not None:
+ warnings.warn(
+ 'channel_post_updates is deprecated. See https://git.io/fxJuV ' 'for more info',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+ if channel_post_updates is False:
+ self.filters &= ~Filters.update.channel_post
+
+ if edited_updates is not None:
+ warnings.warn(
+ 'edited_updates is deprecated. See https://git.io/fxJuV for more info',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+ if edited_updates is False:
+ self.filters &= ~(
+ Filters.update.edited_message | Filters.update.edited_channel_post
+ )
+
+ def check_update(self, update: object) -> Optional[Union[bool, Dict[str, list]]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:class:`telegram.Update`): Incoming telegram update.
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
- if isinstance(update, Update) and self._is_allowed_update(update):
-
- if not self.filters:
- res = True
-
- else:
- message = update.effective_message
- if isinstance(self.filters, list):
- res = any(func(message) for func in self.filters)
- else:
- res = self.filters(message)
-
- else:
- res = False
-
- return res
-
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
-
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
-
- """
- optional_args = self.collect_optional_args(dispatcher, update)
-
- return self.callback(dispatcher.bot, update, **optional_args)
+ if isinstance(update, Update) and update.effective_message:
+ return self.filters(update)
+ return None
+
+ def collect_additional_context(
+ self,
+ context: CCT,
+ update: Update,
+ dispatcher: 'Dispatcher',
+ check_result: Optional[Union[bool, Dict[str, object]]],
+ ) -> None:
+ """Adds possible output of data filters to the :class:`CallbackContext`."""
+ if isinstance(check_result, dict):
+ context.update(check_result)
diff --git a/telegramer/include/telegram/ext/messagequeue.py b/telegramer/include/telegram/ext/messagequeue.py
index 946a8de..da2a734 100644
--- a/telegramer/include/telegram/ext/messagequeue.py
+++ b/telegramer/include/telegram/ext/messagequeue.py
@@ -4,7 +4,7 @@
# Tymofii A. Khodniev (thodnev)
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -20,32 +20,27 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/]
"""A throughput-limiting message processor for Telegram bots."""
-from telegram.utils import promise
-
import functools
-import sys
-import time
+import queue as q
import threading
-if sys.version_info.major > 2:
- import queue as q
-else:
- import Queue as q
+import time
+import warnings
+from typing import TYPE_CHECKING, Callable, List, NoReturn
+
+from telegram.ext.utils.promise import Promise
+from telegram.utils.deprecate import TelegramDeprecationWarning
+
+if TYPE_CHECKING:
+ from telegram import Bot
# We need to count < 1s intervals, so the most accurate timer is needed
-# Starting from Python 3.3 we have time.perf_counter which is the clock
-# with the highest resolution available to the system, so let's use it there.
-# In Python 2.7, there's no perf_counter yet, so fallback on what we have:
-# on Windows, the best available is time.clock while time.time is on
-# another platforms (M. Lutz, "Learning Python," 4ed, p.630-634)
-if sys.version_info.major == 3 and sys.version_info.minor >= 3:
- curtime = time.perf_counter # pylint: disable=E1101
-else:
- curtime = time.clock if sys.platform[:3] == 'win' else time.time
+curtime = time.perf_counter
class DelayQueueError(RuntimeError):
"""Indicates processing errors."""
- pass
+
+ __slots__ = ()
class DelayQueue(threading.Thread):
@@ -53,13 +48,9 @@ class DelayQueue(threading.Thread):
Processes callbacks from queue with specified throughput limits. Creates a separate thread to
process callbacks with delays.
- Attributes:
- burst_limit (:obj:`int`): Number of maximum callbacks to process per time-window.
- time_limit (:obj:`int`): Defines width of time-window used when each processing limit is
- calculated.
- exc_route (:obj:`callable`): A callable, accepting 1 positional argument; used to route
- exceptions from processor thread to main thread;
- name (:obj:`str`): Thread's name.
+ .. deprecated:: 13.3
+ :class:`telegram.ext.DelayQueue` in its current form is deprecated and will be reinvented
+ in a future release. See `this thread `_ for a list of known bugs.
Args:
queue (:obj:`Queue`, optional): Used to pass callbacks to thread. Creates ``Queue``
@@ -72,49 +63,65 @@ class DelayQueue(threading.Thread):
route exceptions from processor thread to main thread; is called on `Exception`
subclass exceptions. If not provided, exceptions are routed through dummy handler,
which re-raises them.
- autostart (:obj:`bool`, optional): If True, processor is started immediately after object's
- creation; if ``False``, should be started manually by `start` method. Defaults to True.
+ autostart (:obj:`bool`, optional): If :obj:`True`, processor is started immediately after
+ object's creation; if :obj:`False`, should be started manually by `start` method.
+ Defaults to :obj:`True`.
name (:obj:`str`, optional): Thread's name. Defaults to ``'DelayQueue-N'``, where N is
sequential number of object created.
+ Attributes:
+ burst_limit (:obj:`int`): Number of maximum callbacks to process per time-window.
+ time_limit (:obj:`int`): Defines width of time-window used when each processing limit is
+ calculated.
+ exc_route (:obj:`callable`): A callable, accepting 1 positional argument; used to route
+ exceptions from processor thread to main thread;
+ name (:obj:`str`): Thread's name.
+
"""
_instcnt = 0 # instance counter
- def __init__(self,
- queue=None,
- burst_limit=30,
- time_limit_ms=1000,
- exc_route=None,
- autostart=True,
- name=None):
+ def __init__(
+ self,
+ queue: q.Queue = None,
+ burst_limit: int = 30,
+ time_limit_ms: int = 1000,
+ exc_route: Callable[[Exception], None] = None,
+ autostart: bool = True,
+ name: str = None,
+ ):
+ warnings.warn(
+ 'DelayQueue in its current form is deprecated and will be reinvented in a future '
+ 'release. See https://git.io/JtDbF for a list of known bugs.',
+ category=TelegramDeprecationWarning,
+ )
+
self._queue = queue if queue is not None else q.Queue()
self.burst_limit = burst_limit
self.time_limit = time_limit_ms / 1000
- self.exc_route = (exc_route if exc_route is not None else self._default_exception_handler)
+ self.exc_route = exc_route if exc_route is not None else self._default_exception_handler
self.__exit_req = False # flag to gently exit thread
self.__class__._instcnt += 1
if name is None:
- name = '%s-%s' % (self.__class__.__name__, self.__class__._instcnt)
- super(DelayQueue, self).__init__(name=name)
+ name = f'{self.__class__.__name__}-{self.__class__._instcnt}'
+ super().__init__(name=name)
self.daemon = False
if autostart: # immediately start processing
- super(DelayQueue, self).start()
+ super().start()
- def run(self):
+ def run(self) -> None:
"""
Do not use the method except for unthreaded testing purposes, the method normally is
automatically called by autostart argument.
"""
-
- times = [] # used to store each callable processing time
+ times: List[float] = [] # used to store each callable processing time
while True:
item = self._queue.get()
if self.__exit_req:
return # shutdown thread
# delay routine
- now = curtime()
+ now = time.perf_counter()
t_delta = now - self.time_limit # calculate early to improve perf.
if times and t_delta > times[-1]:
# if last call was before the limit time-window
@@ -133,32 +140,31 @@ def run(self):
except Exception as exc: # re-route any exceptions
self.exc_route(exc) # to prevent thread exit
- def stop(self, timeout=None):
+ def stop(self, timeout: float = None) -> None:
"""Used to gently stop processor and shutdown its thread.
Args:
timeout (:obj:`float`): Indicates maximum time to wait for processor to stop and its
thread to exit. If timeout exceeds and processor has not stopped, method silently
returns. :attr:`is_alive` could be used afterwards to check the actual status.
- ``timeout`` set to None, blocks until processor is shut down. Defaults to None.
+ ``timeout`` set to :obj:`None`, blocks until processor is shut down.
+ Defaults to :obj:`None`.
"""
-
self.__exit_req = True # gently request
self._queue.put(None) # put something to unfreeze if frozen
- super(DelayQueue, self).join(timeout=timeout)
+ super().join(timeout=timeout)
@staticmethod
- def _default_exception_handler(exc):
+ def _default_exception_handler(exc: Exception) -> NoReturn:
"""
Dummy exception handler which re-raises exception in thread. Could be possibly overwritten
by subclasses.
"""
-
raise exc
- def __call__(self, func, *args, **kwargs):
+ def __call__(self, func: Callable, *args: object, **kwargs: object) -> None:
"""Used to process callbacks in throughput-limiting thread through queue.
Args:
@@ -168,25 +174,28 @@ def __call__(self, func, *args, **kwargs):
**kwargs (:obj:`dict`): Arbitrary keyword-arguments to `func`.
"""
-
if not self.is_alive() or self.__exit_req:
raise DelayQueueError('Could not process callback in stopped thread')
self._queue.put((func, args, kwargs))
-# The most straightforward way to implement this is to use 2 sequenital delay
+# The most straightforward way to implement this is to use 2 sequential delay
# queues, like on classic delay chain schematics in electronics.
# So, message path is:
# msg --> group delay if group msg, else no delay --> normal msg delay --> out
# This way OS threading scheduler cares of timings accuracy.
# (see time.time, time.clock, time.perf_counter, time.sleep @ docs.python.org)
-class MessageQueue(object):
+class MessageQueue:
"""
Implements callback processing with proper delays to avoid hitting Telegram's message limits.
Contains two ``DelayQueue``, for group and for all messages, interconnected in delay chain.
Callables are processed through *group* ``DelayQueue``, then through *all* ``DelayQueue`` for
group-type messages. For non-group messages, only the *all* ``DelayQueue`` is used.
+ .. deprecated:: 13.3
+ :class:`telegram.ext.MessageQueue` in its current form is deprecated and will be reinvented
+ in a future release. See `this thread `_ for a list of known bugs.
+
Args:
all_burst_limit (:obj:`int`, optional): Number of maximum *all-type* callbacks to process
per time-window defined by :attr:`all_time_limit_ms`. Defaults to 30.
@@ -200,56 +209,67 @@ class MessageQueue(object):
to route exceptions from processor threads to main thread; is called on ``Exception``
subclass exceptions. If not provided, exceptions are routed through dummy handler,
which re-raises them.
- autostart (:obj:`bool`, optional): If True, processors are started immediately after
- object's creation; if ``False``, should be started manually by :attr:`start` method.
- Defaults to ``True``.
+ autostart (:obj:`bool`, optional): If :obj:`True`, processors are started immediately after
+ object's creation; if :obj:`False`, should be started manually by :attr:`start` method.
+ Defaults to :obj:`True`.
"""
- def __init__(self,
- all_burst_limit=30,
- all_time_limit_ms=1000,
- group_burst_limit=20,
- group_time_limit_ms=60000,
- exc_route=None,
- autostart=True):
- # create accoring delay queues, use composition
+ def __init__(
+ self,
+ all_burst_limit: int = 30,
+ all_time_limit_ms: int = 1000,
+ group_burst_limit: int = 20,
+ group_time_limit_ms: int = 60000,
+ exc_route: Callable[[Exception], None] = None,
+ autostart: bool = True,
+ ):
+ warnings.warn(
+ 'MessageQueue in its current form is deprecated and will be reinvented in a future '
+ 'release. See https://git.io/JtDbF for a list of known bugs.',
+ category=TelegramDeprecationWarning,
+ )
+
+ # create according delay queues, use composition
self._all_delayq = DelayQueue(
burst_limit=all_burst_limit,
time_limit_ms=all_time_limit_ms,
exc_route=exc_route,
- autostart=autostart)
+ autostart=autostart,
+ )
self._group_delayq = DelayQueue(
burst_limit=group_burst_limit,
time_limit_ms=group_time_limit_ms,
exc_route=exc_route,
- autostart=autostart)
+ autostart=autostart,
+ )
- def start(self):
+ def start(self) -> None:
"""Method is used to manually start the ``MessageQueue`` processing."""
self._all_delayq.start()
self._group_delayq.start()
- def stop(self, timeout=None):
+ def stop(self, timeout: float = None) -> None:
+ """Stops the ``MessageQueue``."""
self._group_delayq.stop(timeout=timeout)
self._all_delayq.stop(timeout=timeout)
- stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docsting if any
+ stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docstring if any
- def __call__(self, promise, is_group_msg=False):
+ def __call__(self, promise: Callable, is_group_msg: bool = False) -> Callable:
"""
- Processes callables in troughput-limiting queues to avoid hitting limits (specified with
+ Processes callables in throughput-limiting queues to avoid hitting limits (specified with
:attr:`burst_limit` and :attr:`time_limit`.
Args:
promise (:obj:`callable`): Mainly the ``telegram.utils.promise.Promise`` (see Notes for
other callables), that is processed in delay queues.
is_group_msg (:obj:`bool`, optional): Defines whether ``promise`` would be processed in
- group*+*all* ``DelayQueue``s (if set to ``True``), or only through *all*
- ``DelayQueue`` (if set to ``False``), resulting in needed delays to avoid
- hitting specified limits. Defaults to ``True``.
+ group*+*all* ``DelayQueue``s (if set to :obj:`True`), or only through *all*
+ ``DelayQueue`` (if set to :obj:`False`), resulting in needed delays to avoid
+ hitting specified limits. Defaults to :obj:`False`.
- Notes:
+ Note:
Method is designed to accept ``telegram.utils.promise.Promise`` as ``promise``
argument, but other callables could be used too. For example, lambdas or simple
functions could be used to wrap original func to be called with needed args. In that
@@ -260,7 +280,6 @@ def __call__(self, promise, is_group_msg=False):
:obj:`callable`: Used as ``promise`` argument.
"""
-
if not is_group_msg: # ignore middle group delay
self._all_delayq(promise)
else: # use middle group delay
@@ -268,7 +287,7 @@ def __call__(self, promise, is_group_msg=False):
return promise
-def queuedmessage(method):
+def queuedmessage(method: Callable) -> Callable:
"""A decorator to be used with :attr:`telegram.Bot` send* methods.
Note:
@@ -287,12 +306,12 @@ def queuedmessage(method):
Wrapped method starts accepting the next kwargs:
Args:
- queued (:obj:`bool`, optional): If set to ``True``, the ``MessageQueue`` is used to process
- output messages. Defaults to `self._is_queued_out`.
- isgroup (:obj:`bool`, optional): If set to ``True``, the message is meant to be group-type
- (as there's no obvious way to determine its type in other way at the moment).
+ queued (:obj:`bool`, optional): If set to :obj:`True`, the ``MessageQueue`` is used to
+ process output messages. Defaults to `self._is_queued_out`.
+ isgroup (:obj:`bool`, optional): If set to :obj:`True`, the message is meant to be
+ group-type(as there's no obvious way to determine its type in other way at the moment).
Group-type messages could have additional processing delay according to limits set
- in `self._out_queue`. Defaults to ``False``.
+ in `self._out_queue`. Defaults to :obj:`False`.
Returns:
``telegram.utils.promise.Promise``: In case call is queued or original method's return
@@ -301,12 +320,15 @@ def queuedmessage(method):
"""
@functools.wraps(method)
- def wrapped(self, *args, **kwargs):
- queued = kwargs.pop('queued', self._is_messages_queued_default)
+ def wrapped(self: 'Bot', *args: object, **kwargs: object) -> object:
+ # pylint: disable=W0212
+ queued = kwargs.pop(
+ 'queued', self._is_messages_queued_default # type: ignore[attr-defined]
+ )
isgroup = kwargs.pop('isgroup', False)
if queued:
- prom = promise.Promise(method, (self, ) + args, kwargs)
- return self._msg_queue(prom, isgroup)
+ prom = Promise(method, (self,) + args, kwargs)
+ return self._msg_queue(prom, isgroup) # type: ignore[attr-defined]
return method(self, *args, **kwargs)
return wrapped
diff --git a/telegramer/include/telegram/ext/picklepersistence.py b/telegramer/include/telegram/ext/picklepersistence.py
new file mode 100644
index 0000000..c3e4ba8
--- /dev/null
+++ b/telegramer/include/telegram/ext/picklepersistence.py
@@ -0,0 +1,463 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains the PicklePersistence class."""
+import pickle
+from collections import defaultdict
+from typing import (
+ Any,
+ Dict,
+ Optional,
+ Tuple,
+ overload,
+ cast,
+ DefaultDict,
+)
+
+from telegram.ext import BasePersistence
+from .utils.types import UD, CD, BD, ConversationDict, CDCData
+from .contexttypes import ContextTypes
+
+
+class PicklePersistence(BasePersistence[UD, CD, BD]):
+ """Using python's builtin pickle for making your bot persistent.
+
+ Warning:
+ :class:`PicklePersistence` will try to replace :class:`telegram.Bot` instances by
+ :attr:`REPLACED_BOT` and insert the bot set with
+ :meth:`telegram.ext.BasePersistence.set_bot` upon loading of the data. This is to ensure
+ that changes to the bot apply to the saved objects, too. If you change the bots token, this
+ may lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see
+ :meth:`telegram.ext.BasePersistence.replace_bot` and
+ :meth:`telegram.ext.BasePersistence.insert_bot`.
+
+ Args:
+ filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file`
+ is :obj:`False` this will be used as a prefix.
+ store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
+ persistence class. Default is :obj:`True`.
+ store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this
+ persistence class. Default is :obj:`True`.
+ store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this
+ persistence class. Default is :obj:`True`.
+ store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this
+ persistence class. Default is :obj:`False`.
+
+ .. versionadded:: 13.6
+ single_file (:obj:`bool`, optional): When :obj:`False` will store 5 separate files of
+ `filename_user_data`, `filename_bot_data`, `filename_chat_data`,
+ `filename_callback_data` and `filename_conversations`. Default is :obj:`True`.
+ on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when
+ :meth:`flush` is called and keep data in memory until that happens. When
+ :obj:`False` will store data on any transaction *and* on call to :meth:`flush`.
+ Default is :obj:`False`.
+ context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance
+ of :class:`telegram.ext.ContextTypes` to customize the types used in the
+ ``context`` interface. If not passed, the defaults documented in
+ :class:`telegram.ext.ContextTypes` will be used.
+
+ .. versionadded:: 13.6
+
+ Attributes:
+ filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file`
+ is :obj:`False` this will be used as a prefix.
+ store_user_data (:obj:`bool`): Optional. Whether user_data should be saved by this
+ persistence class.
+ store_chat_data (:obj:`bool`): Optional. Whether chat_data should be saved by this
+ persistence class.
+ store_bot_data (:obj:`bool`): Optional. Whether bot_data should be saved by this
+ persistence class.
+ store_callback_data (:obj:`bool`): Optional. Whether callback_data be saved by this
+ persistence class.
+
+ .. versionadded:: 13.6
+ single_file (:obj:`bool`): Optional. When :obj:`False` will store 5 separate files of
+ `filename_user_data`, `filename_bot_data`, `filename_chat_data`,
+ `filename_callback_data` and `filename_conversations`. Default is :obj:`True`.
+ on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when
+ :meth:`flush` is called and keep data in memory until that happens. When
+ :obj:`False` will store data on any transaction *and* on call to :meth:`flush`.
+ Default is :obj:`False`.
+ context_types (:class:`telegram.ext.ContextTypes`): Container for the types used
+ in the ``context`` interface.
+
+ .. versionadded:: 13.6
+ """
+
+ __slots__ = (
+ 'filename',
+ 'single_file',
+ 'on_flush',
+ 'user_data',
+ 'chat_data',
+ 'bot_data',
+ 'callback_data',
+ 'conversations',
+ 'context_types',
+ )
+
+ @overload
+ def __init__(
+ self: 'PicklePersistence[Dict, Dict, Dict]',
+ filename: str,
+ store_user_data: bool = True,
+ store_chat_data: bool = True,
+ store_bot_data: bool = True,
+ single_file: bool = True,
+ on_flush: bool = False,
+ store_callback_data: bool = False,
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'PicklePersistence[UD, CD, BD]',
+ filename: str,
+ store_user_data: bool = True,
+ store_chat_data: bool = True,
+ store_bot_data: bool = True,
+ single_file: bool = True,
+ on_flush: bool = False,
+ store_callback_data: bool = False,
+ context_types: ContextTypes[Any, UD, CD, BD] = None,
+ ):
+ ...
+
+ def __init__(
+ self,
+ filename: str,
+ store_user_data: bool = True,
+ store_chat_data: bool = True,
+ store_bot_data: bool = True,
+ single_file: bool = True,
+ on_flush: bool = False,
+ store_callback_data: bool = False,
+ context_types: ContextTypes[Any, UD, CD, BD] = None,
+ ):
+ super().__init__(
+ store_user_data=store_user_data,
+ store_chat_data=store_chat_data,
+ store_bot_data=store_bot_data,
+ store_callback_data=store_callback_data,
+ )
+ self.filename = filename
+ self.single_file = single_file
+ self.on_flush = on_flush
+ self.user_data: Optional[DefaultDict[int, UD]] = None
+ self.chat_data: Optional[DefaultDict[int, CD]] = None
+ self.bot_data: Optional[BD] = None
+ self.callback_data: Optional[CDCData] = None
+ self.conversations: Optional[Dict[str, Dict[Tuple, object]]] = None
+ self.context_types = cast(ContextTypes[Any, UD, CD, BD], context_types or ContextTypes())
+
+ def _load_singlefile(self) -> None:
+ try:
+ filename = self.filename
+ with open(self.filename, "rb") as file:
+ data = pickle.load(file)
+ self.user_data = defaultdict(self.context_types.user_data, data['user_data'])
+ self.chat_data = defaultdict(self.context_types.chat_data, data['chat_data'])
+ # For backwards compatibility with files not containing bot data
+ self.bot_data = data.get('bot_data', self.context_types.bot_data())
+ self.callback_data = data.get('callback_data', {})
+ self.conversations = data['conversations']
+ except OSError:
+ self.conversations = {}
+ self.user_data = defaultdict(self.context_types.user_data)
+ self.chat_data = defaultdict(self.context_types.chat_data)
+ self.bot_data = self.context_types.bot_data()
+ self.callback_data = None
+ except pickle.UnpicklingError as exc:
+ raise TypeError(f"File {filename} does not contain valid pickle data") from exc
+ except Exception as exc:
+ raise TypeError(f"Something went wrong unpickling {filename}") from exc
+
+ @staticmethod
+ def _load_file(filename: str) -> Any:
+ try:
+ with open(filename, "rb") as file:
+ return pickle.load(file)
+ except OSError:
+ return None
+ except pickle.UnpicklingError as exc:
+ raise TypeError(f"File {filename} does not contain valid pickle data") from exc
+ except Exception as exc:
+ raise TypeError(f"Something went wrong unpickling {filename}") from exc
+
+ def _dump_singlefile(self) -> None:
+ with open(self.filename, "wb") as file:
+ data = {
+ 'conversations': self.conversations,
+ 'user_data': self.user_data,
+ 'chat_data': self.chat_data,
+ 'bot_data': self.bot_data,
+ 'callback_data': self.callback_data,
+ }
+ pickle.dump(data, file)
+
+ @staticmethod
+ def _dump_file(filename: str, data: object) -> None:
+ with open(filename, "wb") as file:
+ pickle.dump(data, file)
+
+ def get_user_data(self) -> DefaultDict[int, UD]:
+ """Returns the user_data from the pickle file if it exists or an empty :obj:`defaultdict`.
+
+ Returns:
+ DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.UD`]: The restored user data.
+ """
+ if self.user_data:
+ pass
+ elif not self.single_file:
+ filename = f"{self.filename}_user_data"
+ data = self._load_file(filename)
+ if not data:
+ data = defaultdict(self.context_types.user_data)
+ else:
+ data = defaultdict(self.context_types.user_data, data)
+ self.user_data = data
+ else:
+ self._load_singlefile()
+ return self.user_data # type: ignore[return-value]
+
+ def get_chat_data(self) -> DefaultDict[int, CD]:
+ """Returns the chat_data from the pickle file if it exists or an empty :obj:`defaultdict`.
+
+ Returns:
+ DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.CD`]: The restored chat data.
+ """
+ if self.chat_data:
+ pass
+ elif not self.single_file:
+ filename = f"{self.filename}_chat_data"
+ data = self._load_file(filename)
+ if not data:
+ data = defaultdict(self.context_types.chat_data)
+ else:
+ data = defaultdict(self.context_types.chat_data, data)
+ self.chat_data = data
+ else:
+ self._load_singlefile()
+ return self.chat_data # type: ignore[return-value]
+
+ def get_bot_data(self) -> BD:
+ """Returns the bot_data from the pickle file if it exists or an empty object of type
+ :class:`telegram.ext.utils.types.BD`.
+
+ Returns:
+ :class:`telegram.ext.utils.types.BD`: The restored bot data.
+ """
+ if self.bot_data:
+ pass
+ elif not self.single_file:
+ filename = f"{self.filename}_bot_data"
+ data = self._load_file(filename)
+ if not data:
+ data = self.context_types.bot_data()
+ self.bot_data = data
+ else:
+ self._load_singlefile()
+ return self.bot_data # type: ignore[return-value]
+
+ def get_callback_data(self) -> Optional[CDCData]:
+ """Returns the callback data from the pickle file if it exists or :obj:`None`.
+
+ .. versionadded:: 13.6
+
+ Returns:
+ Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or
+ :obj:`None`, if no data was stored.
+ """
+ if self.callback_data:
+ pass
+ elif not self.single_file:
+ filename = f"{self.filename}_callback_data"
+ data = self._load_file(filename)
+ if not data:
+ data = None
+ self.callback_data = data
+ else:
+ self._load_singlefile()
+ if self.callback_data is None:
+ return None
+ return self.callback_data[0], self.callback_data[1].copy()
+
+ def get_conversations(self, name: str) -> ConversationDict:
+ """Returns the conversations from the pickle file if it exists or an empty dict.
+
+ Args:
+ name (:obj:`str`): The handlers name.
+
+ Returns:
+ :obj:`dict`: The restored conversations for the handler.
+ """
+ if self.conversations:
+ pass
+ elif not self.single_file:
+ filename = f"{self.filename}_conversations"
+ data = self._load_file(filename)
+ if not data:
+ data = {name: {}}
+ self.conversations = data
+ else:
+ self._load_singlefile()
+ return self.conversations.get(name, {}).copy() # type: ignore[union-attr]
+
+ def update_conversation(
+ self, name: str, key: Tuple[int, ...], new_state: Optional[object]
+ ) -> None:
+ """Will update the conversations for the given handler and depending on :attr:`on_flush`
+ save the pickle file.
+
+ Args:
+ name (:obj:`str`): The handler's name.
+ key (:obj:`tuple`): The key the state is changed for.
+ new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
+ """
+ if not self.conversations:
+ self.conversations = {}
+ if self.conversations.setdefault(name, {}).get(key) == new_state:
+ return
+ self.conversations[name][key] = new_state
+ if not self.on_flush:
+ if not self.single_file:
+ filename = f"{self.filename}_conversations"
+ self._dump_file(filename, self.conversations)
+ else:
+ self._dump_singlefile()
+
+ def update_user_data(self, user_id: int, data: UD) -> None:
+ """Will update the user_data and depending on :attr:`on_flush` save the pickle file.
+
+ Args:
+ user_id (:obj:`int`): The user the data might have been changed for.
+ data (:class:`telegram.ext.utils.types.UD`): The
+ :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``.
+ """
+ if self.user_data is None:
+ self.user_data = defaultdict(self.context_types.user_data)
+ if self.user_data.get(user_id) == data:
+ return
+ self.user_data[user_id] = data
+ if not self.on_flush:
+ if not self.single_file:
+ filename = f"{self.filename}_user_data"
+ self._dump_file(filename, self.user_data)
+ else:
+ self._dump_singlefile()
+
+ def update_chat_data(self, chat_id: int, data: CD) -> None:
+ """Will update the chat_data and depending on :attr:`on_flush` save the pickle file.
+
+ Args:
+ chat_id (:obj:`int`): The chat the data might have been changed for.
+ data (:class:`telegram.ext.utils.types.CD`): The
+ :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``.
+ """
+ if self.chat_data is None:
+ self.chat_data = defaultdict(self.context_types.chat_data)
+ if self.chat_data.get(chat_id) == data:
+ return
+ self.chat_data[chat_id] = data
+ if not self.on_flush:
+ if not self.single_file:
+ filename = f"{self.filename}_chat_data"
+ self._dump_file(filename, self.chat_data)
+ else:
+ self._dump_singlefile()
+
+ def update_bot_data(self, data: BD) -> None:
+ """Will update the bot_data and depending on :attr:`on_flush` save the pickle file.
+
+ Args:
+ data (:class:`telegram.ext.utils.types.BD`): The
+ :attr:`telegram.ext.Dispatcher.bot_data`.
+ """
+ if self.bot_data == data:
+ return
+ self.bot_data = data
+ if not self.on_flush:
+ if not self.single_file:
+ filename = f"{self.filename}_bot_data"
+ self._dump_file(filename, self.bot_data)
+ else:
+ self._dump_singlefile()
+
+ def update_callback_data(self, data: CDCData) -> None:
+ """Will update the callback_data (if changed) and depending on :attr:`on_flush` save the
+ pickle file.
+
+ .. versionadded:: 13.6
+
+ Args:
+ data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore
+ :class:`telegram.ext.CallbackDataCache`.
+ """
+ if self.callback_data == data:
+ return
+ self.callback_data = (data[0], data[1].copy())
+ if not self.on_flush:
+ if not self.single_file:
+ filename = f"{self.filename}_callback_data"
+ self._dump_file(filename, self.callback_data)
+ else:
+ self._dump_singlefile()
+
+ def refresh_user_data(self, user_id: int, user_data: UD) -> None:
+ """Does nothing.
+
+ .. versionadded:: 13.6
+ .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data`
+ """
+
+ def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None:
+ """Does nothing.
+
+ .. versionadded:: 13.6
+ .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data`
+ """
+
+ def refresh_bot_data(self, bot_data: BD) -> None:
+ """Does nothing.
+
+ .. versionadded:: 13.6
+ .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data`
+ """
+
+ def flush(self) -> None:
+ """Will save all data in memory to pickle file(s)."""
+ if self.single_file:
+ if (
+ self.user_data
+ or self.chat_data
+ or self.bot_data
+ or self.callback_data
+ or self.conversations
+ ):
+ self._dump_singlefile()
+ else:
+ if self.user_data:
+ self._dump_file(f"{self.filename}_user_data", self.user_data)
+ if self.chat_data:
+ self._dump_file(f"{self.filename}_chat_data", self.chat_data)
+ if self.bot_data:
+ self._dump_file(f"{self.filename}_bot_data", self.bot_data)
+ if self.callback_data:
+ self._dump_file(f"{self.filename}_callback_data", self.callback_data)
+ if self.conversations:
+ self._dump_file(f"{self.filename}_conversations", self.conversations)
diff --git a/telegramer/include/telegram/ext/pollanswerhandler.py b/telegramer/include/telegram/ext/pollanswerhandler.py
new file mode 100644
index 0000000..53172b2
--- /dev/null
+++ b/telegramer/include/telegram/ext/pollanswerhandler.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains the PollAnswerHandler class."""
+
+
+from telegram import Update
+
+from .handler import Handler
+from .utils.types import CCT
+
+
+class PollAnswerHandler(Handler[Update, CCT]):
+ """Handler class to handle Telegram updates that contain a poll answer.
+
+ Note:
+ :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
+ can use to keep any data in will be sent to the :attr:`callback` function. Related to
+ either the user or the chat that the update was sent in. For each update from the same user
+ or in the same chat, it will be the same ``dict``.
+
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
+ Args:
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``update_queue`` will be passed to the callback function. It will be the ``Queue``
+ instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``job_queue`` will be passed to the callback function. It will be a
+ :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+
+ """
+
+ __slots__ = ()
+
+ def check_update(self, update: object) -> bool:
+ """Determines whether an update should be passed to this handlers :attr:`callback`.
+
+ Args:
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
+
+ Returns:
+ :obj:`bool`
+
+ """
+ return isinstance(update, Update) and bool(update.poll_answer)
diff --git a/telegramer/include/telegram/ext/pollhandler.py b/telegramer/include/telegram/ext/pollhandler.py
new file mode 100644
index 0000000..0e2dee6
--- /dev/null
+++ b/telegramer/include/telegram/ext/pollhandler.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains the PollHandler classes."""
+
+
+from telegram import Update
+
+from .handler import Handler
+from .utils.types import CCT
+
+
+class PollHandler(Handler[Update, CCT]):
+ """Handler class to handle Telegram updates that contain a poll.
+
+ Note:
+ :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
+ can use to keep any data in will be sent to the :attr:`callback` function. Related to
+ either the user or the chat that the update was sent in. For each update from the same user
+ or in the same chat, it will be the same ``dict``.
+
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
+ Args:
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``update_queue`` will be passed to the callback function. It will be the ``Queue``
+ instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``job_queue`` will be passed to the callback function. It will be a
+ :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+
+ """
+
+ __slots__ = ()
+
+ def check_update(self, update: object) -> bool:
+ """Determines whether an update should be passed to this handlers :attr:`callback`.
+
+ Args:
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
+
+ Returns:
+ :obj:`bool`
+
+ """
+ return isinstance(update, Update) and bool(update.poll)
diff --git a/telegramer/include/telegram/ext/precheckoutqueryhandler.py b/telegramer/include/telegram/ext/precheckoutqueryhandler.py
index e3bbe7c..bbef18e 100644
--- a/telegramer/include/telegram/ext/precheckoutqueryhandler.py
+++ b/telegramer/include/telegram/ext/precheckoutqueryhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,81 +18,81 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the PreCheckoutQueryHandler class."""
+
from telegram import Update
+
from .handler import Handler
+from .utils.types import CCT
-class PreCheckoutQueryHandler(Handler):
+class PreCheckoutQueryHandler(Handler[Update, CCT]):
"""Handler class to handle Telegram PreCheckout callback queries.
- Attributes:
- callback (:obj:`callable`): The callback function for this handler.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
- pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
- the callback function.
- pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
- the callback function.
-
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
can use to keep any data in will be sent to the :attr:`callback` function. Related to
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
Args:
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
+ DEPRECATED: Please switch to context based callbacks.
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
- pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``user_data`` will be passed to the callback function. Default is ``False``.
- pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``chat_data`` will be passed to the callback function. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
"""
- def __init__(self,
- callback,
- pass_update_queue=False,
- pass_job_queue=False,
- pass_user_data=False,
- pass_chat_data=False):
- super(PreCheckoutQueryHandler, self).__init__(
- callback,
- pass_update_queue=pass_update_queue,
- pass_job_queue=pass_job_queue,
- pass_user_data=pass_user_data,
- pass_chat_data=pass_chat_data)
-
- def check_update(self, update):
+ __slots__ = ()
+
+ def check_update(self, update: object) -> bool:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:class:`telegram.Update`): Incoming telegram update.
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
- return isinstance(update, Update) and update.pre_checkout_query
-
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
-
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
-
- """
- optional_args = self.collect_optional_args(dispatcher, update)
- return self.callback(dispatcher.bot, update, **optional_args)
+ return isinstance(update, Update) and bool(update.pre_checkout_query)
diff --git a/telegramer/include/telegram/ext/regexhandler.py b/telegramer/include/telegram/ext/regexhandler.py
index 71c5821..8211d83 100644
--- a/telegramer/include/telegram/ext/regexhandler.py
+++ b/telegramer/include/telegram/ext/regexhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,164 +19,148 @@
# TODO: Remove allow_edited
"""This module contains the RegexHandler class."""
-import re
import warnings
+from typing import TYPE_CHECKING, Callable, Dict, Optional, Pattern, TypeVar, Union, Any
-# REMREM from future.utils import string_types
-try:
- from future.utils import string_types
-except Exception as e:
- pass
+from telegram import Update
+from telegram.ext import Filters, MessageHandler
+from telegram.utils.deprecate import TelegramDeprecationWarning
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+from telegram.ext.utils.types import CCT
-try:
- string_types
-except NameError:
- string_types = str
+if TYPE_CHECKING:
+ from telegram.ext import Dispatcher
-from telegram import Update
-from .handler import Handler
+RT = TypeVar('RT')
-class RegexHandler(Handler):
+class RegexHandler(MessageHandler):
"""Handler class to handle Telegram updates based on a regex.
It uses a regular expression to check text messages. Read the documentation of the ``re``
module for more information. The ``re.match`` function is used to determine if an update should
be handled by this handler.
- Attributes:
- pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
- callback (:obj:`callable`): The callback function for this handler.
- pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the
- callback function.
- pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to
- the callback function.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
- pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
- the callback function.
- pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
- the callback function.
-
Note:
- :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
- can use to keep any data in will be sent to the :attr:`callback` function. Related to
- either the user or the chat that the update was sent in. For each update from the same user
- or in the same chat, it will be the same ``dict``.
+ This handler is being deprecated. For the same use case use:
+ ``MessageHandler(Filters.regex(r'pattern'), callback)``
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
Args:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
pass_groups (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groups()`` as a keyword argument called ``groups``.
- Default is ``False``
+ Default is :obj:`False`
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
- Default is ``False``
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ Default is :obj:`False`
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
- pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``user_data`` will be passed to the callback function. Default is ``False``.
- pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``chat_data`` will be passed to the callback function. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
message_updates (:obj:`bool`, optional): Should "normal" message updates be handled?
- Default is ``True``.
+ Default is :obj:`True`.
channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled?
- Default is ``True``.
+ Default is :obj:`True`.
edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default
- is ``False``.
- allow_edited (:obj:`bool`, optional): If the handler should also accept edited messages.
- Default is ``False`` - Deprecated. use edited_updates instead.
+ is :obj:`False`.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
Raises:
ValueError
+ Attributes:
+ pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the
+ callback function.
+ pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to
+ the callback function.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+
"""
- def __init__(self,
- pattern,
- callback,
- pass_groups=False,
- pass_groupdict=False,
- pass_update_queue=False,
- pass_job_queue=False,
- pass_user_data=False,
- pass_chat_data=False,
- allow_edited=False,
- message_updates=True,
- channel_post_updates=False,
- edited_updates=False
- ):
- if not message_updates and not channel_post_updates and not edited_updates:
- raise ValueError(
- 'message_updates, channel_post_updates and edited_updates are all False')
- if allow_edited:
- warnings.warn('allow_edited is getting deprecated, please use edited_updates instead')
- edited_updates = allow_edited
-
- super(RegexHandler, self).__init__(
+ __slots__ = ('pass_groups', 'pass_groupdict')
+
+ def __init__(
+ self,
+ pattern: Union[str, Pattern],
+ callback: Callable[[Update, CCT], RT],
+ pass_groups: bool = False,
+ pass_groupdict: bool = False,
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ pass_user_data: bool = False,
+ pass_chat_data: bool = False,
+ allow_edited: bool = False, # pylint: disable=W0613
+ message_updates: bool = True,
+ channel_post_updates: bool = False,
+ edited_updates: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
+ warnings.warn(
+ 'RegexHandler is deprecated. See https://git.io/fxJuV for more info',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+ super().__init__(
+ Filters.regex(pattern),
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
- pass_chat_data=pass_chat_data)
-
- if isinstance(pattern, string_types):
- pattern = re.compile(pattern)
-
- self.pattern = pattern
+ pass_chat_data=pass_chat_data,
+ message_updates=message_updates,
+ channel_post_updates=channel_post_updates,
+ edited_updates=edited_updates,
+ run_async=run_async,
+ )
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
- self.allow_edited = allow_edited
- self.message_updates = message_updates
- self.channel_post_updates = channel_post_updates
- self.edited_updates = edited_updates
-
- def check_update(self, update):
- """Determines whether an update should be passed to this handlers :attr:`callback`.
-
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
-
- Returns:
- :obj:`bool`
+ def collect_optional_args(
+ self,
+ dispatcher: 'Dispatcher',
+ update: Update = None,
+ check_result: Optional[Union[bool, Dict[str, Any]]] = None,
+ ) -> Dict[str, object]:
+ """Pass the results of ``re.match(pattern, text).{groups(), groupdict()}`` to the
+ callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if
+ needed.
"""
- if not isinstance(update, Update) and not update.effective_message:
- return False
- if any([self.message_updates and update.message,
- self.edited_updates and (update.edited_message or update.edited_channel_post),
- self.channel_post_updates and update.channel_post]) and \
- update.effective_message.text:
- match = re.match(self.pattern, update.effective_message.text)
- return bool(match)
- return False
-
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
-
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
-
- """
-
- optional_args = self.collect_optional_args(dispatcher, update)
- match = re.match(self.pattern, update.effective_message.text)
-
- if self.pass_groups:
- optional_args['groups'] = match.groups()
- if self.pass_groupdict:
- optional_args['groupdict'] = match.groupdict()
-
- return self.callback(dispatcher.bot, update, **optional_args)
+ optional_args = super().collect_optional_args(dispatcher, update, check_result)
+ if isinstance(check_result, dict):
+ if self.pass_groups:
+ optional_args['groups'] = check_result['matches'][0].groups()
+ if self.pass_groupdict:
+ optional_args['groupdict'] = check_result['matches'][0].groupdict()
+ return optional_args
diff --git a/telegramer/include/telegram/ext/shippingqueryhandler.py b/telegramer/include/telegram/ext/shippingqueryhandler.py
index 663a3f7..d8b0218 100644
--- a/telegramer/include/telegram/ext/shippingqueryhandler.py
+++ b/telegramer/include/telegram/ext/shippingqueryhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,81 +18,80 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the ShippingQueryHandler class."""
+
from telegram import Update
from .handler import Handler
+from .utils.types import CCT
-class ShippingQueryHandler(Handler):
+class ShippingQueryHandler(Handler[Update, CCT]):
"""Handler class to handle Telegram shipping callback queries.
- Attributes:
- callback (:obj:`callable`): The callback function for this handler.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
- pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
- the callback function.
- pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
- the callback function.
-
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
can use to keep any data in will be sent to the :attr:`callback` function. Related to
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
+ Note that this is DEPRECATED, and you should use context based callbacks. See
+ https://git.io/fxJuV for more info.
+
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
+
Args:
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
- pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``user_data`` will be passed to the callback function. Default is ``False``.
- pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
- ``chat_data`` will be passed to the callback function. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``user_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
+ ``chat_data`` will be passed to the callback function. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
+ the callback function.
+ pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
"""
- def __init__(self,
- callback,
- pass_update_queue=False,
- pass_job_queue=False,
- pass_user_data=False,
- pass_chat_data=False):
- super(ShippingQueryHandler, self).__init__(
- callback,
- pass_update_queue=pass_update_queue,
- pass_job_queue=pass_job_queue,
- pass_user_data=pass_user_data,
- pass_chat_data=pass_chat_data)
-
- def check_update(self, update):
+ __slots__ = ()
+
+ def check_update(self, update: object) -> bool:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:class:`telegram.Update`): Incoming telegram update.
+ update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
- return isinstance(update, Update) and update.shipping_query
-
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
-
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
-
- """
- optional_args = self.collect_optional_args(dispatcher, update)
- return self.callback(dispatcher.bot, update, **optional_args)
+ return isinstance(update, Update) and bool(update.shipping_query)
diff --git a/telegramer/include/telegram/ext/stringcommandhandler.py b/telegramer/include/telegram/ext/stringcommandhandler.py
index 00e3dd1..e3945ae 100644
--- a/telegramer/include/telegram/ext/stringcommandhandler.py
+++ b/telegramer/include/telegram/ext/stringcommandhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,90 +18,132 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the StringCommandHandler class."""
-# REMREM from future.utils import string_types
-try:
- from future.utils import string_types
-except Exception as e:
- pass
+from typing import TYPE_CHECKING, Callable, Dict, List, Optional, TypeVar, Union
+
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .handler import Handler
+from .utils.types import CCT
+
+if TYPE_CHECKING:
+ from telegram.ext import Dispatcher
+RT = TypeVar('RT')
-class StringCommandHandler(Handler):
+
+class StringCommandHandler(Handler[str, CCT]):
"""Handler class to handle string commands. Commands are string updates that start with ``/``.
+ The handler will add a ``list`` to the
+ :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings,
+ which is the text following the command split on single whitespace characters.
Note:
This handler is not used to handle Telegram :attr:`telegram.Update`, but strings manually
put in the queue. For example to send messages with the bot using command line or API.
- Attributes:
- command (:obj:`str`): The command this handler should listen for.
- callback (:obj:`callable`): The callback function for this handler.
- pass_args (:obj:`bool`): Optional. Determines whether the handler should be passed
- ``args``.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
-
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
command (:obj:`str`): The command this handler should listen for.
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that a command should be
- processed by this handler.
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the
arguments passed to the command as a keyword argument called ``args``. It will contain
a list of strings, which is the text following the command split on single or
- consecutive whitespace characters. Default is ``False``
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ consecutive whitespace characters. Default is :obj:`False`
+ DEPRECATED: Please switch to context based callbacks.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ command (:obj:`str`): The command this handler should listen for.
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_args (:obj:`bool`): Determines whether the handler should be passed
+ ``args``.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
"""
- def __init__(self,
- command,
- callback,
- pass_args=False,
- pass_update_queue=False,
- pass_job_queue=False):
- super(StringCommandHandler, self).__init__(
- callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue)
+ __slots__ = ('command', 'pass_args')
+
+ def __init__(
+ self,
+ command: str,
+ callback: Callable[[str, CCT], RT],
+ pass_args: bool = False,
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
+ super().__init__(
+ callback,
+ pass_update_queue=pass_update_queue,
+ pass_job_queue=pass_job_queue,
+ run_async=run_async,
+ )
self.command = command
self.pass_args = pass_args
- def check_update(self, update):
+ def check_update(self, update: object) -> Optional[List[str]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:obj:`str`): An incomming command.
+ update (:obj:`object`): The incoming update.
Returns:
:obj:`bool`
"""
-
- return (isinstance(update, string_types) and update.startswith('/')
- and update[1:].split(' ')[0] == self.command)
-
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
-
- Args:
- update (:obj:`str`): An incomming command.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the command.
-
+ if isinstance(update, str) and update.startswith('/'):
+ args = update[1:].split(' ')
+ if args[0] == self.command:
+ return args[1:]
+ return None
+
+ def collect_optional_args(
+ self,
+ dispatcher: 'Dispatcher',
+ update: str = None,
+ check_result: Optional[List[str]] = None,
+ ) -> Dict[str, object]:
+ """Provide text after the command to the callback the ``args`` argument as list, split on
+ single whitespaces.
"""
-
- optional_args = self.collect_optional_args(dispatcher)
-
+ optional_args = super().collect_optional_args(dispatcher, update, check_result)
if self.pass_args:
- optional_args['args'] = update.split()[1:]
-
- return self.callback(dispatcher.bot, update, **optional_args)
+ optional_args['args'] = check_result
+ return optional_args
+
+ def collect_additional_context(
+ self,
+ context: CCT,
+ update: str,
+ dispatcher: 'Dispatcher',
+ check_result: Optional[List[str]],
+ ) -> None:
+ """Add text after the command to :attr:`CallbackContext.args` as list, split on single
+ whitespaces.
+ """
+ context.args = check_result
diff --git a/telegramer/include/telegram/ext/stringregexhandler.py b/telegramer/include/telegram/ext/stringregexhandler.py
index 0131bdc..9be6b36 100644
--- a/telegramer/include/telegram/ext/stringregexhandler.py
+++ b/telegramer/include/telegram/ext/stringregexhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,22 +19,20 @@
"""This module contains the StringRegexHandler class."""
import re
+from typing import TYPE_CHECKING, Callable, Dict, Match, Optional, Pattern, TypeVar, Union
-# REMREM from future.utils import string_types
-try:
- from future.utils import string_types
-except Exception as e:
- pass
-
-try:
- string_types
-except NameError:
- string_types = str
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .handler import Handler
+from .utils.types import CCT
+
+if TYPE_CHECKING:
+ from telegram.ext import Dispatcher
+RT = TypeVar('RT')
-class StringRegexHandler(Handler):
+
+class StringRegexHandler(Handler[str, CCT]):
"""Handler class to handle string updates based on a regex which checks the update content.
Read the documentation of the ``re`` module for more information. The ``re.match`` function is
@@ -44,83 +42,125 @@ class StringRegexHandler(Handler):
This handler is not used to handle Telegram :attr:`telegram.Update`, but strings manually
put in the queue. For example to send messages with the bot using command line or API.
- Attributes:
- pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
- callback (:obj:`callable`): The callback function for this handler.
- pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the
- callback function.
- pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to
- the callback function.
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
pass_groups (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groups()`` as a keyword argument called ``groups``.
- Default is ``False``
+ Default is :obj:`False`
+ DEPRECATED: Please switch to context based callbacks.
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
- Default is ``False``
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ Default is :obj:`False`
+ DEPRECATED: Please switch to context based callbacks.
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
+
+ Attributes:
+ pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
+ callback (:obj:`callable`): The callback function for this handler.
+ pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the
+ callback function.
+ pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to
+ the callback function.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
"""
- def __init__(self,
- pattern,
- callback,
- pass_groups=False,
- pass_groupdict=False,
- pass_update_queue=False,
- pass_job_queue=False):
- super(StringRegexHandler, self).__init__(
- callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue)
-
- if isinstance(pattern, string_types):
+ __slots__ = ('pass_groups', 'pass_groupdict', 'pattern')
+
+ def __init__(
+ self,
+ pattern: Union[str, Pattern],
+ callback: Callable[[str, CCT], RT],
+ pass_groups: bool = False,
+ pass_groupdict: bool = False,
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
+ super().__init__(
+ callback,
+ pass_update_queue=pass_update_queue,
+ pass_job_queue=pass_job_queue,
+ run_async=run_async,
+ )
+
+ if isinstance(pattern, str):
pattern = re.compile(pattern)
self.pattern = pattern
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
- def check_update(self, update):
+ def check_update(self, update: object) -> Optional[Match]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:obj:`str`): An incomming command.
+ update (:obj:`object`): The incoming update.
Returns:
:obj:`bool`
"""
- return isinstance(update, string_types) and bool(re.match(self.pattern, update))
-
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
-
- Args:
- update (:obj:`str`): An incomming command.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the command.
-
+ if isinstance(update, str):
+ match = re.match(self.pattern, update)
+ if match:
+ return match
+ return None
+
+ def collect_optional_args(
+ self,
+ dispatcher: 'Dispatcher',
+ update: str = None,
+ check_result: Optional[Match] = None,
+ ) -> Dict[str, object]:
+ """Pass the results of ``re.match(pattern, update).{groups(), groupdict()}`` to the
+ callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if
+ needed.
"""
- optional_args = self.collect_optional_args(dispatcher)
- match = re.match(self.pattern, update)
-
- if self.pass_groups:
- optional_args['groups'] = match.groups()
- if self.pass_groupdict:
- optional_args['groupdict'] = match.groupdict()
-
- return self.callback(dispatcher.bot, update, **optional_args)
+ optional_args = super().collect_optional_args(dispatcher, update, check_result)
+ if self.pattern:
+ if self.pass_groups and check_result:
+ optional_args['groups'] = check_result.groups()
+ if self.pass_groupdict and check_result:
+ optional_args['groupdict'] = check_result.groupdict()
+ return optional_args
+
+ def collect_additional_context(
+ self,
+ context: CCT,
+ update: str,
+ dispatcher: 'Dispatcher',
+ check_result: Optional[Match],
+ ) -> None:
+ """Add the result of ``re.match(pattern, update)`` to :attr:`CallbackContext.matches` as
+ list with one element.
+ """
+ if self.pattern and check_result:
+ context.matches = [check_result]
diff --git a/telegramer/include/telegram/ext/typehandler.py b/telegramer/include/telegram/ext/typehandler.py
index bb388e4..14029ab 100644
--- a/telegramer/include/telegram/ext/typehandler.py
+++ b/telegramer/include/telegram/ext/typehandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,72 +18,91 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the TypeHandler class."""
+from typing import Callable, Type, TypeVar, Union
+from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
+
from .handler import Handler
+from .utils.types import CCT
+
+RT = TypeVar('RT')
+UT = TypeVar('UT')
-class TypeHandler(Handler):
+class TypeHandler(Handler[UT, CCT]):
"""Handler class to handle updates of custom types.
- Attributes:
- type (:obj:`type`): The ``type`` of updates this handler should process.
- callback (:obj:`callable`): The callback function for this handler.
- strict (:obj:`bool`): Optional. Use ``type`` instead of ``isinstance``.
- Default is ``False``
- pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
- passed to the callback function.
- pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
- the callback function.
+ Warning:
+ When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
+ attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
type (:obj:`type`): The ``type`` of updates this handler should process, as
determined by ``isinstance``
- callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
- It will be called when the :attr:`check_update` has determined that an update should be
- processed by this handler.
+ callback (:obj:`callable`): The callback function for this handler. Will be called when
+ :attr:`check_update` has determined that an update should be processed by this handler.
+ Callback signature for context based API:
+
+ ``def callback(update: Update, context: CallbackContext)``
+
+ The return value of the callback is usually ignored except for the special case of
+ :class:`telegram.ext.ConversationHandler`.
strict (:obj:`bool`, optional): Use ``type`` instead of ``isinstance``.
- Default is ``False``
- pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ Default is :obj:`False`
+ pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
- that contains new updates which can be used to insert updates. Default is ``False``.
- pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
+ that contains new updates which can be used to insert updates. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
- which can be used to schedule new jobs. Default is ``False``.
+ which can be used to schedule new jobs. Default is :obj:`False`.
+ DEPRECATED: Please switch to context based callbacks.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
+ Defaults to :obj:`False`.
- """
+ Attributes:
+ type (:obj:`type`): The ``type`` of updates this handler should process.
+ callback (:obj:`callable`): The callback function for this handler.
+ strict (:obj:`bool`): Use ``type`` instead of ``isinstance``. Default is :obj:`False`.
+ pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
+ passed to the callback function.
+ pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
+ the callback function.
+ run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
- def __init__(self, type, callback, strict=False, pass_update_queue=False,
- pass_job_queue=False):
- super(TypeHandler, self).__init__(
- callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue)
- self.type = type
- self.strict = strict
+ """
- def check_update(self, update):
+ __slots__ = ('type', 'strict')
+
+ def __init__(
+ self,
+ type: Type[UT], # pylint: disable=W0622
+ callback: Callable[[UT, CCT], RT],
+ strict: bool = False,
+ pass_update_queue: bool = False,
+ pass_job_queue: bool = False,
+ run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
+ ):
+ super().__init__(
+ callback,
+ pass_update_queue=pass_update_queue,
+ pass_job_queue=pass_job_queue,
+ run_async=run_async,
+ )
+ self.type = type # pylint: disable=E0237
+ self.strict = strict # pylint: disable=E0237
+
+ def check_update(self, update: object) -> bool:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
- update (:class:`telegram.Update`): Incoming telegram update.
+ update (:obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
-
if not self.strict:
return isinstance(update, self.type)
- else:
- return type(update) is self.type
-
- def handle_update(self, update, dispatcher):
- """Send the update to the :attr:`callback`.
-
- Args:
- update (:class:`telegram.Update`): Incoming telegram update.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
-
- """
- optional_args = self.collect_optional_args(dispatcher)
-
- return self.callback(dispatcher.bot, update, **optional_args)
+ return type(update) is self.type # pylint: disable=C0123
diff --git a/telegramer/include/telegram/ext/updater.py b/telegramer/include/telegram/ext/updater.py
index 7aa44cf..b2c0512 100644
--- a/telegramer/include/telegram/ext/updater.py
+++ b/telegramer/include/telegram/ext/updater.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,26 +19,40 @@
"""This module contains the class Updater, which tries to make creating Telegram bots intuitive."""
import logging
-import os
import ssl
-from threading import Thread, Lock, current_thread, Event
+import warnings
+from queue import Queue
+from signal import SIGABRT, SIGINT, SIGTERM, signal
+from threading import Event, Lock, Thread, current_thread
from time import sleep
-import subprocess
-from signal import signal, SIGINT, SIGTERM, SIGABRT
-# REMREM from queue import Queue
-from Queue import Queue
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ List,
+ Optional,
+ Tuple,
+ Union,
+ no_type_check,
+ Generic,
+ overload,
+)
from telegram import Bot, TelegramError
-from telegram.ext import Dispatcher, JobQueue
-from telegram.error import Unauthorized, InvalidToken, RetryAfter, TimedOut
-from telegram.utils.helpers import get_signal_name
+from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized
+from telegram.ext import Dispatcher, JobQueue, ContextTypes, ExtBot
+from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated
+from telegram.utils.helpers import get_signal_name, DEFAULT_FALSE, DefaultValue
from telegram.utils.request import Request
-from telegram.utils.webhookhandler import (WebhookServer, WebhookHandler)
+from telegram.ext.utils.types import CCT, UD, CD, BD
+from telegram.ext.utils.webhookhandler import WebhookAppClass, WebhookServer
-logging.getLogger(__name__).addHandler(logging.NullHandler())
+if TYPE_CHECKING:
+ from telegram.ext import BasePersistence, Defaults, CallbackContext
-class Updater(object):
+class Updater(Generic[CCT, UD, CD, BD]):
"""
This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to
:class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to
@@ -49,150 +63,354 @@ class Updater(object):
production, use a webhook to receive updates. This is achieved using the WebhookServer and
WebhookHandler classes.
+ Note:
+ * You must supply either a :attr:`bot` or a :attr:`token` argument.
+ * If you supply a :attr:`bot`, you will need to pass :attr:`arbitrary_callback_data`,
+ and :attr:`defaults` to the bot instead of the :class:`telegram.ext.Updater`. In this
+ case, you'll have to use the class :class:`telegram.ext.ExtBot`.
- Attributes:
- bot (:class:`telegram.Bot`): The bot used with this Updater.
- user_sig_handler (:obj:`signal`): signals the updater will respond to.
- update_queue (:obj:`Queue`): Queue for the updates.
- job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater.
- dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and
- dispatches them to the handlers.
- running (:obj:`bool`): Indicates if the updater is running.
+ .. versionchanged:: 13.6
Args:
token (:obj:`str`, optional): The bot's token given by the @BotFather.
base_url (:obj:`str`, optional): Base_url for the bot.
+ base_file_url (:obj:`str`, optional): Base_file_url for the bot.
workers (:obj:`int`, optional): Amount of threads in the thread pool for functions
- decorated with ``@run_async``.
- bot (:class:`telegram.Bot`, optional): A pre-initialized bot instance. If a pre-initialized
- bot is used, it is the user's responsibility to create it using a `Request`
- instance with a large enough connection pool.
+ decorated with ``@run_async`` (ignored if `dispatcher` argument is used).
+ bot (:class:`telegram.Bot`, optional): A pre-initialized bot instance (ignored if
+ `dispatcher` argument is used). If a pre-initialized bot is used, it is the user's
+ responsibility to create it using a `Request` instance with a large enough connection
+ pool.
+ dispatcher (:class:`telegram.ext.Dispatcher`, optional): A pre-initialized dispatcher
+ instance. If a pre-initialized dispatcher is used, it is the user's responsibility to
+ create it with proper arguments.
private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data.
private_key_password (:obj:`bytes`, optional): Password for above private key.
user_sig_handler (:obj:`function`, optional): Takes ``signum, frame`` as positional
arguments. This will be called when a signal is received, defaults are (SIGINT,
- SIGTERM, SIGABRT) setable with :attr:`idle`.
+ SIGTERM, SIGABRT) settable with :attr:`idle`.
request_kwargs (:obj:`dict`, optional): Keyword args to control the creation of a
- `telegram.utils.request.Request` object (ignored if `bot` argument is used). The
- request_kwargs are very useful for the advanced users who would like to control the
- default timeouts and/or control the proxy used for http communication.
-
- Note:
- You must supply either a :attr:`bot` or a :attr:`token` argument.
+ `telegram.utils.request.Request` object (ignored if `bot` or `dispatcher` argument is
+ used). The request_kwargs are very useful for the advanced users who would like to
+ control the default timeouts and/or control the proxy used for http communication.
+ use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback
+ API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`.
+ **New users**: set this to :obj:`True`.
+ persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
+ store data that should be persistent over restarts (ignored if `dispatcher` argument is
+ used).
+ defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to
+ be used if not set explicitly in the bot methods.
+ arbitrary_callback_data (:obj:`bool` | :obj:`int` | :obj:`None`, optional): Whether to
+ allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`.
+ Pass an integer to specify the maximum number of cached objects. For more details,
+ please see our wiki. Defaults to :obj:`False`.
+
+ .. versionadded:: 13.6
+ context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance
+ of :class:`telegram.ext.ContextTypes` to customize the types used in the
+ ``context`` interface. If not passed, the defaults documented in
+ :class:`telegram.ext.ContextTypes` will be used.
+
+ .. versionadded:: 13.6
Raises:
ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them.
+
+ Attributes:
+ bot (:class:`telegram.Bot`): The bot used with this Updater.
+ user_sig_handler (:obj:`function`): Optional. Function to be called when a signal is
+ received.
+ update_queue (:obj:`Queue`): Queue for the updates.
+ job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater.
+ dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and
+ dispatches them to the handlers.
+ running (:obj:`bool`): Indicates if the updater is running.
+ persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
+ store data that should be persistent over restarts.
+ use_context (:obj:`bool`): Optional. :obj:`True` if using context based callbacks.
+
"""
- _request = None
-
- def __init__(self,
- token=None,
- base_url=None,
- workers=4,
- bot=None,
- private_key=None,
- private_key_password=None,
- user_sig_handler=None,
- request_kwargs=None):
-
- if (token is None) and (bot is None):
- raise ValueError('`token` or `bot` must be passed')
- if (token is not None) and (bot is not None):
- raise ValueError('`token` and `bot` are mutually exclusive')
- if (private_key is not None) and (bot is not None):
- raise ValueError('`bot` and `private_key` are mutually exclusive')
+ __slots__ = (
+ 'persistence',
+ 'dispatcher',
+ 'user_sig_handler',
+ 'bot',
+ 'logger',
+ 'update_queue',
+ 'job_queue',
+ '__exception_event',
+ 'last_update_id',
+ 'running',
+ '_request',
+ 'is_idle',
+ 'httpd',
+ '__lock',
+ '__threads',
+ '__dict__',
+ )
+
+ @overload
+ def __init__(
+ self: 'Updater[CallbackContext, dict, dict, dict]',
+ token: str = None,
+ base_url: str = None,
+ workers: int = 4,
+ bot: Bot = None,
+ private_key: bytes = None,
+ private_key_password: bytes = None,
+ user_sig_handler: Callable = None,
+ request_kwargs: Dict[str, Any] = None,
+ persistence: 'BasePersistence' = None, # pylint: disable=E0601
+ defaults: 'Defaults' = None,
+ use_context: bool = True,
+ base_file_url: str = None,
+ arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE,
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'Updater[CCT, UD, CD, BD]',
+ token: str = None,
+ base_url: str = None,
+ workers: int = 4,
+ bot: Bot = None,
+ private_key: bytes = None,
+ private_key_password: bytes = None,
+ user_sig_handler: Callable = None,
+ request_kwargs: Dict[str, Any] = None,
+ persistence: 'BasePersistence' = None,
+ defaults: 'Defaults' = None,
+ use_context: bool = True,
+ base_file_url: str = None,
+ arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE,
+ context_types: ContextTypes[CCT, UD, CD, BD] = None,
+ ):
+ ...
+
+ @overload
+ def __init__(
+ self: 'Updater[CCT, UD, CD, BD]',
+ user_sig_handler: Callable = None,
+ dispatcher: Dispatcher[CCT, UD, CD, BD] = None,
+ ):
+ ...
+
+ def __init__( # type: ignore[no-untyped-def,misc]
+ self,
+ token: str = None,
+ base_url: str = None,
+ workers: int = 4,
+ bot: Bot = None,
+ private_key: bytes = None,
+ private_key_password: bytes = None,
+ user_sig_handler: Callable = None,
+ request_kwargs: Dict[str, Any] = None,
+ persistence: 'BasePersistence' = None,
+ defaults: 'Defaults' = None,
+ use_context: bool = True,
+ dispatcher=None,
+ base_file_url: str = None,
+ arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE,
+ context_types: ContextTypes[CCT, UD, CD, BD] = None,
+ ):
+
+ if defaults and bot:
+ warnings.warn(
+ 'Passing defaults to an Updater has no effect when a Bot is passed '
+ 'as well. Pass them to the Bot instead.',
+ TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+ if arbitrary_callback_data is not DEFAULT_FALSE and bot:
+ warnings.warn(
+ 'Passing arbitrary_callback_data to an Updater has no '
+ 'effect when a Bot is passed as well. Pass them to the Bot instead.',
+ stacklevel=2,
+ )
+
+ if dispatcher is None:
+ if (token is None) and (bot is None):
+ raise ValueError('`token` or `bot` must be passed')
+ if (token is not None) and (bot is not None):
+ raise ValueError('`token` and `bot` are mutually exclusive')
+ if (private_key is not None) and (bot is not None):
+ raise ValueError('`bot` and `private_key` are mutually exclusive')
+ else:
+ if bot is not None:
+ raise ValueError('`dispatcher` and `bot` are mutually exclusive')
+ if persistence is not None:
+ raise ValueError('`dispatcher` and `persistence` are mutually exclusive')
+ if use_context != dispatcher.use_context:
+ raise ValueError('`dispatcher` and `use_context` are mutually exclusive')
+ if context_types is not None:
+ raise ValueError('`dispatcher` and `context_types` are mutually exclusive')
+ if workers is not None:
+ raise ValueError('`dispatcher` and `workers` are mutually exclusive')
self.logger = logging.getLogger(__name__)
+ self._request = None
+
+ if dispatcher is None:
+ con_pool_size = workers + 4
+
+ if bot is not None:
+ self.bot = bot
+ if bot.request.con_pool_size < con_pool_size:
+ self.logger.warning(
+ 'Connection pool of Request object is smaller than optimal value (%s)',
+ con_pool_size,
+ )
+ else:
+ # we need a connection pool the size of:
+ # * for each of the workers
+ # * 1 for Dispatcher
+ # * 1 for polling Updater (even if webhook is used, we can spare a connection)
+ # * 1 for JobQueue
+ # * 1 for main thread
+ if request_kwargs is None:
+ request_kwargs = {}
+ if 'con_pool_size' not in request_kwargs:
+ request_kwargs['con_pool_size'] = con_pool_size
+ self._request = Request(**request_kwargs)
+ self.bot = ExtBot(
+ token, # type: ignore[arg-type]
+ base_url,
+ base_file_url=base_file_url,
+ request=self._request,
+ private_key=private_key,
+ private_key_password=private_key_password,
+ defaults=defaults,
+ arbitrary_callback_data=(
+ False # type: ignore[arg-type]
+ if arbitrary_callback_data is DEFAULT_FALSE
+ else arbitrary_callback_data
+ ),
+ )
+ self.update_queue: Queue = Queue()
+ self.job_queue = JobQueue()
+ self.__exception_event = Event()
+ self.persistence = persistence
+ self.dispatcher = Dispatcher(
+ self.bot,
+ self.update_queue,
+ job_queue=self.job_queue,
+ workers=workers,
+ exception_event=self.__exception_event,
+ persistence=persistence,
+ use_context=use_context,
+ context_types=context_types,
+ )
+ self.job_queue.set_dispatcher(self.dispatcher)
+ else:
+ con_pool_size = dispatcher.workers + 4
- con_pool_size = workers + 4
-
- if bot is not None:
- self.bot = bot
- if bot.request.con_pool_size < con_pool_size:
+ self.bot = dispatcher.bot
+ if self.bot.request.con_pool_size < con_pool_size:
self.logger.warning(
'Connection pool of Request object is smaller than optimal value (%s)',
- con_pool_size)
- else:
- # we need a connection pool the size of:
- # * for each of the workers
- # * 1 for Dispatcher
- # * 1 for polling Updater (even if webhook is used, we can spare a connection)
- # * 1 for JobQueue
- # * 1 for main thread
- if request_kwargs is None:
- request_kwargs = {}
- if 'con_pool_size' not in request_kwargs:
- request_kwargs['con_pool_size'] = con_pool_size
- self._request = Request(**request_kwargs)
- self.bot = Bot(token, base_url, request=self._request, private_key=private_key,
- private_key_password=private_key_password)
+ con_pool_size,
+ )
+ self.update_queue = dispatcher.update_queue
+ self.__exception_event = dispatcher.exception_event
+ self.persistence = dispatcher.persistence
+ self.job_queue = dispatcher.job_queue
+ self.dispatcher = dispatcher
+
self.user_sig_handler = user_sig_handler
- self.update_queue = Queue()
- self.job_queue = JobQueue(self.bot)
- self.__exception_event = Event()
- self.dispatcher = Dispatcher(
- self.bot,
- self.update_queue,
- job_queue=self.job_queue,
- workers=workers,
- exception_event=self.__exception_event)
self.last_update_id = 0
self.running = False
self.is_idle = False
self.httpd = None
self.__lock = Lock()
- self.__threads = []
-
- def _init_thread(self, target, name, *args, **kwargs):
- thr = Thread(target=self._thread_wrapper, name=name, args=(target,) + args, kwargs=kwargs)
+ self.__threads: List[Thread] = []
+
+ def __setattr__(self, key: str, value: object) -> None:
+ if key.startswith('__'):
+ key = f"_{self.__class__.__name__}{key}"
+ if issubclass(self.__class__, Updater) and self.__class__ is not Updater:
+ object.__setattr__(self, key, value)
+ return
+ set_new_attribute_deprecated(self, key, value)
+
+ def _init_thread(self, target: Callable, name: str, *args: object, **kwargs: object) -> None:
+ thr = Thread(
+ target=self._thread_wrapper,
+ name=f"Bot:{self.bot.id}:{name}",
+ args=(target,) + args,
+ kwargs=kwargs,
+ )
thr.start()
self.__threads.append(thr)
- def _thread_wrapper(self, target, *args, **kwargs):
+ def _thread_wrapper(self, target: Callable, *args: object, **kwargs: object) -> None:
thr_name = current_thread().name
- self.logger.debug('{0} - started'.format(thr_name))
+ self.logger.debug('%s - started', thr_name)
try:
target(*args, **kwargs)
except Exception:
self.__exception_event.set()
self.logger.exception('unhandled exception in %s', thr_name)
raise
- self.logger.debug('{0} - ended'.format(thr_name))
-
- def start_polling(self,
- poll_interval=0.0,
- timeout=10,
- clean=False,
- bootstrap_retries=-1,
- read_latency=2.,
- allowed_updates=None):
+ self.logger.debug('%s - ended', thr_name)
+
+ def start_polling(
+ self,
+ poll_interval: float = 0.0,
+ timeout: float = 10,
+ clean: bool = None,
+ bootstrap_retries: int = -1,
+ read_latency: float = 2.0,
+ allowed_updates: List[str] = None,
+ drop_pending_updates: bool = None,
+ ) -> Optional[Queue]:
"""Starts polling updates from Telegram.
Args:
poll_interval (:obj:`float`, optional): Time to wait between polling updates from
- Telegram in seconds. Default is 0.0.
- timeout (:obj:`float`, optional): Passed to :attr:`telegram.Bot.get_updates`.
- clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers
- before actually starting to poll. Default is False.
+ Telegram in seconds. Default is ``0.0``.
+ timeout (:obj:`float`, optional): Passed to :meth:`telegram.Bot.get_updates`.
+ drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
+ Telegram servers before actually starting to poll. Default is :obj:`False`.
+
+ .. versionadded :: 13.4
+ clean (:obj:`bool`, optional): Alias for ``drop_pending_updates``.
+
+ .. deprecated:: 13.4
+ Use ``drop_pending_updates`` instead.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
- `Updater` will retry on failures on the Telegram server.
+ :class:`telegram.ext.Updater` will retry on failures on the Telegram server.
* < 0 - retry indefinitely (default)
* 0 - no retries
* > 0 - retry up to X times
allowed_updates (List[:obj:`str`], optional): Passed to
- :attr:`telegram.Bot.get_updates`.
+ :meth:`telegram.Bot.get_updates`.
read_latency (:obj:`float` | :obj:`int`, optional): Grace time in seconds for receiving
- the reply from server. Will be added to the `timeout` value and used as the read
- timeout from server (Default: 2).
+ the reply from server. Will be added to the ``timeout`` value and used as the read
+ timeout from server (Default: ``2``).
Returns:
:obj:`Queue`: The update queue that can be filled from the main thread.
"""
+ if (clean is not None) and (drop_pending_updates is not None):
+ raise TypeError('`clean` and `drop_pending_updates` are mutually exclusive.')
+
+ if clean is not None:
+ warnings.warn(
+ 'The argument `clean` of `start_polling` is deprecated. Please use '
+ '`drop_pending_updates` instead.',
+ category=TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+
+ drop_pending_updates = drop_pending_updates if drop_pending_updates is not None else clean
+
with self.__lock:
if not self.running:
self.running = True
@@ -200,31 +418,54 @@ def start_polling(self,
# Create & start threads
self.job_queue.start()
dispatcher_ready = Event()
+ polling_ready = Event()
self._init_thread(self.dispatcher.start, "dispatcher", ready=dispatcher_ready)
- self._init_thread(self._start_polling, "updater", poll_interval, timeout,
- read_latency, bootstrap_retries, clean, allowed_updates)
-
+ self._init_thread(
+ self._start_polling,
+ "updater",
+ poll_interval,
+ timeout,
+ read_latency,
+ bootstrap_retries,
+ drop_pending_updates,
+ allowed_updates,
+ ready=polling_ready,
+ )
+
+ self.logger.debug('Waiting for Dispatcher and polling to start')
dispatcher_ready.wait()
+ polling_ready.wait()
# Return the update queue so the main thread can insert updates
return self.update_queue
-
- def start_webhook(self,
- listen='127.0.0.1',
- port=80,
- url_path='',
- cert=None,
- key=None,
- clean=False,
- bootstrap_retries=0,
- webhook_url=None,
- allowed_updates=None):
+ return None
+
+ def start_webhook(
+ self,
+ listen: str = '127.0.0.1',
+ port: int = 80,
+ url_path: str = '',
+ cert: str = None,
+ key: str = None,
+ clean: bool = None,
+ bootstrap_retries: int = 0,
+ webhook_url: str = None,
+ allowed_updates: List[str] = None,
+ force_event_loop: bool = None,
+ drop_pending_updates: bool = None,
+ ip_address: str = None,
+ max_connections: int = 40,
+ ) -> Optional[Queue]:
"""
- Starts a small http server to listen for updates via webhook. If cert
- and key are not provided, the webhook will be started directly on
+ Starts a small http server to listen for updates via webhook. If :attr:`cert`
+ and :attr:`key` are not provided, the webhook will be started directly on
http://listen:port/url_path, so SSL can be handled by another
application. Else, the webhook will be started on
- https://listen:port/url_path
+ https://listen:port/url_path. Also calls :meth:`telegram.Bot.set_webhook` as required.
+
+ .. versionchanged:: 13.4
+ :meth:`start_webhook` now *always* calls :meth:`telegram.Bot.set_webhook`, so pass
+ ``webhook_url`` instead of calling ``updater.bot.set_webhook(webhook_url)`` manually.
Args:
listen (:obj:`str`, optional): IP-Address to listen on. Default ``127.0.0.1``.
@@ -232,53 +473,133 @@ def start_webhook(self,
url_path (:obj:`str`, optional): Path inside url.
cert (:obj:`str`, optional): Path to the SSL certificate file.
key (:obj:`str`, optional): Path to the SSL key file.
- clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers
- before actually starting the webhook. Default is ``False``.
+ drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
+ Telegram servers before actually starting to poll. Default is :obj:`False`.
+
+ .. versionadded :: 13.4
+ clean (:obj:`bool`, optional): Alias for ``drop_pending_updates``.
+
+ .. deprecated:: 13.4
+ Use ``drop_pending_updates`` instead.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
- `Updater` will retry on failures on the Telegram server.
+ :class:`telegram.ext.Updater` will retry on failures on the Telegram server.
* < 0 - retry indefinitely (default)
* 0 - no retries
* > 0 - retry up to X times
webhook_url (:obj:`str`, optional): Explicitly specify the webhook url. Useful behind
- NAT, reverse proxy, etc. Default is derived from `listen`, `port` & `url_path`.
+ NAT, reverse proxy, etc. Default is derived from ``listen``, ``port`` &
+ ``url_path``.
+ ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`.
+
+ .. versionadded :: 13.4
allowed_updates (List[:obj:`str`], optional): Passed to
- :attr:`telegram.Bot.set_webhook`.
+ :meth:`telegram.Bot.set_webhook`.
+ force_event_loop (:obj:`bool`, optional): Legacy parameter formerly used for a
+ workaround on Windows + Python 3.8+. No longer has any effect.
+
+ .. deprecated:: 13.6
+ Since version 13.6, ``tornade>=6.1`` is required, which resolves the former
+ issue.
+
+ max_connections (:obj:`int`, optional): Passed to
+ :meth:`telegram.Bot.set_webhook`.
+
+ .. versionadded:: 13.6
Returns:
:obj:`Queue`: The update queue that can be filled from the main thread.
"""
+ if (clean is not None) and (drop_pending_updates is not None):
+ raise TypeError('`clean` and `drop_pending_updates` are mutually exclusive.')
+
+ if clean is not None:
+ warnings.warn(
+ 'The argument `clean` of `start_webhook` is deprecated. Please use '
+ '`drop_pending_updates` instead.',
+ category=TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+
+ if force_event_loop is not None:
+ warnings.warn(
+ 'The argument `force_event_loop` of `start_webhook` is deprecated and no longer '
+ 'has any effect.',
+ category=TelegramDeprecationWarning,
+ stacklevel=2,
+ )
+
+ drop_pending_updates = drop_pending_updates if drop_pending_updates is not None else clean
+
with self.__lock:
if not self.running:
self.running = True
# Create & start threads
+ webhook_ready = Event()
+ dispatcher_ready = Event()
self.job_queue.start()
- self._init_thread(self.dispatcher.start, "dispatcher"),
- self._init_thread(self._start_webhook, "updater", listen, port, url_path, cert,
- key, bootstrap_retries, clean, webhook_url, allowed_updates)
+ self._init_thread(self.dispatcher.start, "dispatcher", dispatcher_ready)
+ self._init_thread(
+ self._start_webhook,
+ "updater",
+ listen,
+ port,
+ url_path,
+ cert,
+ key,
+ bootstrap_retries,
+ drop_pending_updates,
+ webhook_url,
+ allowed_updates,
+ ready=webhook_ready,
+ ip_address=ip_address,
+ max_connections=max_connections,
+ )
+
+ self.logger.debug('Waiting for Dispatcher and Webhook to start')
+ webhook_ready.wait()
+ dispatcher_ready.wait()
# Return the update queue so the main thread can insert updates
return self.update_queue
-
- def _start_polling(self, poll_interval, timeout, read_latency, bootstrap_retries, clean,
- allowed_updates): # pragma: no cover
+ return None
+
+ @no_type_check
+ def _start_polling(
+ self,
+ poll_interval,
+ timeout,
+ read_latency,
+ bootstrap_retries,
+ drop_pending_updates,
+ allowed_updates,
+ ready=None,
+ ): # pragma: no cover
# Thread target of thread 'updater'. Runs in background, pulls
# updates from Telegram and inserts them in the update queue of the
# Dispatcher.
self.logger.debug('Updater thread started (polling)')
- self._bootstrap(bootstrap_retries, clean=clean, webhook_url='', allowed_updates=None)
+ self._bootstrap(
+ bootstrap_retries,
+ drop_pending_updates=drop_pending_updates,
+ webhook_url='',
+ allowed_updates=None,
+ )
self.logger.debug('Bootstrap done')
def polling_action_cb():
updates = self.bot.get_updates(
- self.last_update_id, timeout=timeout, read_latency=read_latency,
- allowed_updates=allowed_updates)
+ self.last_update_id,
+ timeout=timeout,
+ read_latency=read_latency,
+ allowed_updates=allowed_updates,
+ )
if updates:
if not self.running:
@@ -295,14 +616,19 @@ def polling_onerr_cb(exc):
# broadcast it
self.update_queue.put(exc)
- self._network_loop_retry(polling_action_cb, polling_onerr_cb, 'getting Updates',
- poll_interval)
+ if ready is not None:
+ ready.set()
+
+ self._network_loop_retry(
+ polling_action_cb, polling_onerr_cb, 'getting Updates', poll_interval
+ )
+ @no_type_check
def _network_loop_retry(self, action_cb, onerr_cb, description, interval):
"""Perform a loop calling `action_cb`, retrying after network errors.
- Stop condition for loop: `self.running` evaluates False or return value of `action_cb`
- evaluates False.
+ Stop condition for loop: `self.running` evaluates :obj:`False` or return value of
+ `action_cb` evaluates :obj:`False`.
Args:
action_cb (:obj:`callable`): Network oriented callback function to call.
@@ -319,9 +645,9 @@ def _network_loop_retry(self, action_cb, onerr_cb, description, interval):
try:
if not action_cb():
break
- except RetryAfter as e:
- self.logger.info('%s', e)
- cur_interval = 0.5 + e.retry_after
+ except RetryAfter as exc:
+ self.logger.info('%s', exc)
+ cur_interval = 0.5 + exc.retry_after
except TimedOut as toe:
self.logger.debug('Timed out %s: %s', description, toe)
# If failure is due to timeout, we should retry asap.
@@ -329,9 +655,9 @@ def _network_loop_retry(self, action_cb, onerr_cb, description, interval):
except InvalidToken as pex:
self.logger.error('Invalid token; aborting')
raise pex
- except TelegramError as te:
- self.logger.error('Error while %s: %s', description, te)
- onerr_cb(te)
+ except TelegramError as telegram_exc:
+ self.logger.error('Error while %s: %s', description, telegram_exc)
+ onerr_cb(telegram_exc)
cur_interval = self._increase_poll_interval(cur_interval)
else:
cur_interval = interval
@@ -340,123 +666,152 @@ def _network_loop_retry(self, action_cb, onerr_cb, description, interval):
sleep(cur_interval)
@staticmethod
- def _increase_poll_interval(current_interval):
+ def _increase_poll_interval(current_interval: float) -> float:
# increase waiting times on subsequent errors up to 30secs
if current_interval == 0:
current_interval = 1
elif current_interval < 30:
- current_interval += current_interval / 2
- elif current_interval > 30:
- current_interval = 30
+ current_interval *= 1.5
+ else:
+ current_interval = min(30.0, current_interval)
return current_interval
- def _start_webhook(self, listen, port, url_path, cert, key, bootstrap_retries, clean,
- webhook_url, allowed_updates):
+ @no_type_check
+ def _start_webhook(
+ self,
+ listen,
+ port,
+ url_path,
+ cert,
+ key,
+ bootstrap_retries,
+ drop_pending_updates,
+ webhook_url,
+ allowed_updates,
+ ready=None,
+ ip_address=None,
+ max_connections: int = 40,
+ ):
self.logger.debug('Updater thread started (webhook)')
+
+ # Note that we only use the SSL certificate for the WebhookServer, if the key is also
+ # present. This is because the WebhookServer may not actually be in charge of performing
+ # the SSL handshake, e.g. in case a reverse proxy is used
use_ssl = cert is not None and key is not None
+
if not url_path.startswith('/'):
- url_path = '/{0}'.format(url_path)
+ url_path = f'/{url_path}'
- # Create and start server
- self.httpd = WebhookServer((listen, port), WebhookHandler, self.update_queue, url_path,
- self.bot)
+ # Create Tornado app instance
+ app = WebhookAppClass(url_path, self.bot, self.update_queue)
+ # Form SSL Context
+ # An SSLError is raised if the private key does not match with the certificate
if use_ssl:
- self._check_ssl_cert(cert, key)
-
- # DO NOT CHANGE: Only set webhook if SSL is handled by library
- if not webhook_url:
- webhook_url = self._gen_webhook_url(listen, port, url_path)
-
- self._bootstrap(
- max_retries=bootstrap_retries,
- clean=clean,
- webhook_url=webhook_url,
- cert=open(cert, 'rb'),
- allowed_updates=allowed_updates)
- elif clean:
- self.logger.warning("cleaning updates is not supported if "
- "SSL-termination happens elsewhere; skipping")
-
- self.httpd.serve_forever(poll_interval=1)
-
- def _check_ssl_cert(self, cert, key):
- # Check SSL-Certificate with openssl, if possible
- try:
- exit_code = subprocess.call(
- ["openssl", "x509", "-text", "-noout", "-in", cert],
- stdout=open(os.devnull, 'wb'),
- stderr=subprocess.STDOUT)
- except OSError:
- exit_code = 0
- if exit_code == 0:
try:
- self.httpd.socket = ssl.wrap_socket(
- self.httpd.socket, certfile=cert, keyfile=key, server_side=True)
- except ssl.SSLError as error:
- self.logger.exception('Failed to init SSL socket')
- raise TelegramError(str(error))
+ ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ ssl_ctx.load_cert_chain(cert, key)
+ except ssl.SSLError as exc:
+ raise TelegramError('Invalid SSL Certificate') from exc
else:
- raise TelegramError('SSL Certificate invalid')
+ ssl_ctx = None
- @staticmethod
- def _gen_webhook_url(listen, port, url_path):
- return 'https://{listen}:{port}{path}'.format(listen=listen, port=port, path=url_path)
+ # Create and start server
+ self.httpd = WebhookServer(listen, port, app, ssl_ctx)
+
+ if not webhook_url:
+ webhook_url = self._gen_webhook_url(listen, port, url_path)
+
+ # We pass along the cert to the webhook if present.
+ cert_file = open(cert, 'rb') if cert is not None else None
+ self._bootstrap(
+ max_retries=bootstrap_retries,
+ drop_pending_updates=drop_pending_updates,
+ webhook_url=webhook_url,
+ allowed_updates=allowed_updates,
+ cert=cert_file,
+ ip_address=ip_address,
+ max_connections=max_connections,
+ )
+ if cert_file is not None:
+ cert_file.close()
+
+ self.httpd.serve_forever(ready=ready)
- def _bootstrap(self, max_retries, clean, webhook_url, allowed_updates, cert=None,
- bootstrap_interval=5):
+ @staticmethod
+ def _gen_webhook_url(listen: str, port: int, url_path: str) -> str:
+ return f'https://{listen}:{port}{url_path}'
+
+ @no_type_check
+ def _bootstrap(
+ self,
+ max_retries,
+ drop_pending_updates,
+ webhook_url,
+ allowed_updates,
+ cert=None,
+ bootstrap_interval=5,
+ ip_address=None,
+ max_connections: int = 40,
+ ):
retries = [0]
def bootstrap_del_webhook():
- self.bot.delete_webhook()
- return False
-
- def bootstrap_clean_updates():
- self.logger.debug('Cleaning updates from Telegram server')
- updates = self.bot.get_updates()
- while updates:
- updates = self.bot.get_updates(updates[-1].update_id + 1)
+ self.logger.debug('Deleting webhook')
+ if drop_pending_updates:
+ self.logger.debug('Dropping pending updates from Telegram server')
+ self.bot.delete_webhook(drop_pending_updates=drop_pending_updates)
return False
def bootstrap_set_webhook():
+ self.logger.debug('Setting webhook')
+ if drop_pending_updates:
+ self.logger.debug('Dropping pending updates from Telegram server')
self.bot.set_webhook(
- url=webhook_url, certificate=cert, allowed_updates=allowed_updates)
+ url=webhook_url,
+ certificate=cert,
+ allowed_updates=allowed_updates,
+ ip_address=ip_address,
+ drop_pending_updates=drop_pending_updates,
+ max_connections=max_connections,
+ )
return False
def bootstrap_onerr_cb(exc):
if not isinstance(exc, Unauthorized) and (max_retries < 0 or retries[0] < max_retries):
retries[0] += 1
- self.logger.warning('Failed bootstrap phase; try=%s max_retries=%s',
- retries[0], max_retries)
+ self.logger.warning(
+ 'Failed bootstrap phase; try=%s max_retries=%s', retries[0], max_retries
+ )
else:
self.logger.error('Failed bootstrap phase after %s retries (%s)', retries[0], exc)
raise exc
- # Cleaning pending messages is done by polling for them - so we need to delete webhook if
- # one is configured.
- # We also take this chance to delete pre-configured webhook if this is a polling Updater.
- # NOTE: We don't know ahead if a webhook is configured, so we just delete.
- if clean or not webhook_url:
- self._network_loop_retry(bootstrap_del_webhook, bootstrap_onerr_cb,
- 'bootstrap del webhook', bootstrap_interval)
- retries[0] = 0
-
- # Clean pending messages, if requested.
- if clean:
- self._network_loop_retry(bootstrap_clean_updates, bootstrap_onerr_cb,
- 'bootstrap clean updates', bootstrap_interval)
+ # Dropping pending updates from TG can be efficiently done with the drop_pending_updates
+ # parameter of delete/start_webhook, even in the case of polling. Also we want to make
+ # sure that no webhook is configured in case of polling, so we just always call
+ # delete_webhook for polling
+ if drop_pending_updates or not webhook_url:
+ self._network_loop_retry(
+ bootstrap_del_webhook,
+ bootstrap_onerr_cb,
+ 'bootstrap del webhook',
+ bootstrap_interval,
+ )
retries[0] = 0
- sleep(1)
# Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set,
# so we set it anyhow.
if webhook_url:
- self._network_loop_retry(bootstrap_set_webhook, bootstrap_onerr_cb,
- 'bootstrap set webhook', bootstrap_interval)
-
- def stop(self):
+ self._network_loop_retry(
+ bootstrap_set_webhook,
+ bootstrap_onerr_cb,
+ 'bootstrap set webhook',
+ bootstrap_interval,
+ )
+
+ def stop(self) -> None:
"""Stops the polling/webhook thread, the dispatcher and the job queue."""
-
self.job_queue.stop()
with self.__lock:
if self.running or self.dispatcher.has_running_threads:
@@ -472,49 +827,62 @@ def stop(self):
if self._request:
self._request.stop()
- def _stop_httpd(self):
+ @no_type_check
+ def _stop_httpd(self) -> None:
if self.httpd:
- self.logger.debug('Waiting for current webhook connection to be '
- 'closed... Send a Telegram message to the bot to exit '
- 'immediately.')
+ self.logger.debug(
+ 'Waiting for current webhook connection to be '
+ 'closed... Send a Telegram message to the bot to exit '
+ 'immediately.'
+ )
self.httpd.shutdown()
self.httpd = None
- def _stop_dispatcher(self):
+ @no_type_check
+ def _stop_dispatcher(self) -> None:
self.logger.debug('Requesting Dispatcher to stop...')
self.dispatcher.stop()
- def _join_threads(self):
+ @no_type_check
+ def _join_threads(self) -> None:
for thr in self.__threads:
- self.logger.debug('Waiting for {0} thread to end'.format(thr.name))
+ self.logger.debug('Waiting for %s thread to end', thr.name)
thr.join()
- self.logger.debug('{0} thread has ended'.format(thr.name))
+ self.logger.debug('%s thread has ended', thr.name)
self.__threads = []
- def signal_handler(self, signum, frame):
+ @no_type_check
+ def _signal_handler(self, signum, frame) -> None:
self.is_idle = False
if self.running:
- self.logger.info('Received signal {} ({}), stopping...'.format(
- signum, get_signal_name(signum)))
+ self.logger.info(
+ 'Received signal %s (%s), stopping...', signum, get_signal_name(signum)
+ )
+ if self.persistence:
+ # Update user_data, chat_data and bot_data before flushing
+ self.dispatcher.update_persistence()
+ self.persistence.flush()
self.stop()
if self.user_sig_handler:
self.user_sig_handler(signum, frame)
else:
self.logger.warning('Exiting immediately!')
+ # pylint: disable=C0415,W0212
import os
+
os._exit(1)
- def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)):
+ def idle(self, stop_signals: Union[List, Tuple] = (SIGINT, SIGTERM, SIGABRT)) -> None:
"""Blocks until one of the signals are received and stops the updater.
Args:
- stop_signals (:obj:`iterable`): Iterable containing signals from the signal module that
- should be subscribed to. Updater.stop() will be called on receiving one of those
- signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``).
+ stop_signals (:obj:`list` | :obj:`tuple`): List containing signals from the signal
+ module that should be subscribed to. :meth:`Updater.stop()` will be called on
+ receiving one of those signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``).
"""
for sig in stop_signals:
- signal(sig, self.signal_handler)
+ signal(sig, self._signal_handler)
self.is_idle = True
diff --git a/telegramer/include/telegram/ext/utils/__init__.py b/telegramer/include/telegram/ext/utils/__init__.py
new file mode 100644
index 0000000..b624e1e
--- /dev/null
+++ b/telegramer/include/telegram/ext/utils/__init__.py
@@ -0,0 +1,17 @@
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
diff --git a/telegramer/include/telegram/ext/utils/promise.py b/telegramer/include/telegram/ext/utils/promise.py
new file mode 100644
index 0000000..86b3981
--- /dev/null
+++ b/telegramer/include/telegram/ext/utils/promise.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains the Promise class."""
+
+import logging
+from threading import Event
+from typing import Callable, List, Optional, Tuple, TypeVar, Union
+
+from telegram.utils.deprecate import set_new_attribute_deprecated
+from telegram.utils.types import JSONDict
+
+RT = TypeVar('RT')
+
+
+logger = logging.getLogger(__name__)
+
+
+class Promise:
+ """A simple Promise implementation for use with the run_async decorator, DelayQueue etc.
+
+ Args:
+ pooled_function (:obj:`callable`): The callable that will be called concurrently.
+ args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`.
+ kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`.
+ update (:class:`telegram.Update` | :obj:`object`, optional): The update this promise is
+ associated with.
+ error_handling (:obj:`bool`, optional): Whether exceptions raised by :attr:`func`
+ may be handled by error handlers. Defaults to :obj:`True`.
+
+ Attributes:
+ pooled_function (:obj:`callable`): The callable that will be called concurrently.
+ args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`.
+ kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`.
+ done (:obj:`threading.Event`): Is set when the result is available.
+ update (:class:`telegram.Update` | :obj:`object`): Optional. The update this promise is
+ associated with.
+ error_handling (:obj:`bool`): Optional. Whether exceptions raised by :attr:`func`
+ may be handled by error handlers. Defaults to :obj:`True`.
+
+ """
+
+ __slots__ = (
+ 'pooled_function',
+ 'args',
+ 'kwargs',
+ 'update',
+ 'error_handling',
+ 'done',
+ '_done_callback',
+ '_result',
+ '_exception',
+ '__dict__',
+ )
+
+ # TODO: Remove error_handling parameter once we drop the @run_async decorator
+ def __init__(
+ self,
+ pooled_function: Callable[..., RT],
+ args: Union[List, Tuple],
+ kwargs: JSONDict,
+ update: object = None,
+ error_handling: bool = True,
+ ):
+ self.pooled_function = pooled_function
+ self.args = args
+ self.kwargs = kwargs
+ self.update = update
+ self.error_handling = error_handling
+ self.done = Event()
+ self._done_callback: Optional[Callable] = None
+ self._result: Optional[RT] = None
+ self._exception: Optional[Exception] = None
+
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
+
+ def run(self) -> None:
+ """Calls the :attr:`pooled_function` callable."""
+ try:
+ self._result = self.pooled_function(*self.args, **self.kwargs)
+
+ except Exception as exc:
+ self._exception = exc
+
+ finally:
+ self.done.set()
+ if self._exception is None and self._done_callback:
+ try:
+ self._done_callback(self.result())
+ except Exception as exc:
+ logger.warning(
+ "`done_callback` of a Promise raised the following exception."
+ " The exception won't be handled by error handlers."
+ )
+ logger.warning("Full traceback:", exc_info=exc)
+
+ def __call__(self) -> None:
+ self.run()
+
+ def result(self, timeout: float = None) -> Optional[RT]:
+ """Return the result of the ``Promise``.
+
+ Args:
+ timeout (:obj:`float`, optional): Maximum time in seconds to wait for the result to be
+ calculated. ``None`` means indefinite. Default is ``None``.
+
+ Returns:
+ Returns the return value of :attr:`pooled_function` or ``None`` if the ``timeout``
+ expires.
+
+ Raises:
+ object exception raised by :attr:`pooled_function`.
+ """
+ self.done.wait(timeout=timeout)
+ if self._exception is not None:
+ raise self._exception # pylint: disable=raising-bad-type
+ return self._result
+
+ def add_done_callback(self, callback: Callable) -> None:
+ """
+ Callback to be run when :class:`telegram.ext.utils.promise.Promise` becomes done.
+
+ Note:
+ Callback won't be called if :attr:`pooled_function`
+ raises an exception.
+
+ Args:
+ callback (:obj:`callable`): The callable that will be called when promise is done.
+ callback will be called by passing ``Promise.result()`` as only positional argument.
+
+ """
+ if self.done.wait(0):
+ callback(self.result())
+ else:
+ self._done_callback = callback
+
+ @property
+ def exception(self) -> Optional[Exception]:
+ """The exception raised by :attr:`pooled_function` or ``None`` if no exception has been
+ raised (yet).
+ """
+ return self._exception
diff --git a/telegramer/include/telegram/ext/utils/types.py b/telegramer/include/telegram/ext/utils/types.py
new file mode 100644
index 0000000..a63e528
--- /dev/null
+++ b/telegramer/include/telegram/ext/utils/types.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains custom typing aliases.
+
+.. versionadded:: 13.6
+"""
+from typing import TypeVar, TYPE_CHECKING, Tuple, List, Dict, Any, Optional
+
+if TYPE_CHECKING:
+ from telegram.ext import CallbackContext # noqa: F401
+
+
+ConversationDict = Dict[Tuple[int, ...], Optional[object]]
+"""Dicts as maintained by the :class:`telegram.ext.ConversationHandler`.
+
+ .. versionadded:: 13.6
+"""
+
+CDCData = Tuple[List[Tuple[str, float, Dict[str, Any]]], Dict[str, str]]
+"""Tuple[List[Tuple[:obj:`str`, :obj:`float`, Dict[:obj:`str`, :obj:`any`]]], \
+ Dict[:obj:`str`, :obj:`str`]]: Data returned by
+ :attr:`telegram.ext.CallbackDataCache.persistence_data`.
+
+ .. versionadded:: 13.6
+"""
+
+CCT = TypeVar('CCT', bound='CallbackContext')
+"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.
+
+.. versionadded:: 13.6
+"""
+UD = TypeVar('UD')
+"""Type of the user data for a single user.
+
+.. versionadded:: 13.6
+"""
+CD = TypeVar('CD')
+"""Type of the chat data for a single user.
+
+.. versionadded:: 13.6
+"""
+BD = TypeVar('BD')
+"""Type of the bot data.
+
+.. versionadded:: 13.6
+"""
diff --git a/telegramer/include/telegram/ext/utils/webhookhandler.py b/telegramer/include/telegram/ext/utils/webhookhandler.py
new file mode 100644
index 0000000..64e639b
--- /dev/null
+++ b/telegramer/include/telegram/ext/utils/webhookhandler.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=C0114
+
+import logging
+from queue import Queue
+from ssl import SSLContext
+from threading import Event, Lock
+from typing import TYPE_CHECKING, Any, Optional
+
+import tornado.web
+from tornado import httputil
+from tornado.httpserver import HTTPServer
+from tornado.ioloop import IOLoop
+
+from telegram import Update
+from telegram.ext import ExtBot
+from telegram.utils.deprecate import set_new_attribute_deprecated
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+try:
+ import ujson as json
+except ImportError:
+ import json # type: ignore[no-redef]
+
+
+class WebhookServer:
+ __slots__ = (
+ 'http_server',
+ 'listen',
+ 'port',
+ 'loop',
+ 'logger',
+ 'is_running',
+ 'server_lock',
+ 'shutdown_lock',
+ '__dict__',
+ )
+
+ def __init__(
+ self, listen: str, port: int, webhook_app: 'WebhookAppClass', ssl_ctx: SSLContext
+ ):
+ self.http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx)
+ self.listen = listen
+ self.port = port
+ self.loop: Optional[IOLoop] = None
+ self.logger = logging.getLogger(__name__)
+ self.is_running = False
+ self.server_lock = Lock()
+ self.shutdown_lock = Lock()
+
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
+
+ def serve_forever(self, ready: Event = None) -> None:
+ with self.server_lock:
+ IOLoop().make_current()
+ self.is_running = True
+ self.logger.debug('Webhook Server started.')
+ self.loop = IOLoop.current()
+ self.http_server.listen(self.port, address=self.listen)
+
+ if ready is not None:
+ ready.set()
+
+ self.loop.start()
+ self.logger.debug('Webhook Server stopped.')
+ self.is_running = False
+
+ def shutdown(self) -> None:
+ with self.shutdown_lock:
+ if not self.is_running:
+ self.logger.warning('Webhook Server already stopped.')
+ return
+ self.loop.add_callback(self.loop.stop) # type: ignore
+
+ def handle_error(self, request: object, client_address: str) -> None: # pylint: disable=W0613
+ """Handle an error gracefully."""
+ self.logger.debug(
+ 'Exception happened during processing of request from %s',
+ client_address,
+ exc_info=True,
+ )
+
+
+class WebhookAppClass(tornado.web.Application):
+ def __init__(self, webhook_path: str, bot: 'Bot', update_queue: Queue):
+ self.shared_objects = {"bot": bot, "update_queue": update_queue}
+ handlers = [(rf"{webhook_path}/?", WebhookHandler, self.shared_objects)] # noqa
+ tornado.web.Application.__init__(self, handlers) # type: ignore
+
+ def log_request(self, handler: tornado.web.RequestHandler) -> None: # skipcq: PTC-W0049
+ pass
+
+
+# WebhookHandler, process webhook calls
+# pylint: disable=W0223
+class WebhookHandler(tornado.web.RequestHandler):
+ SUPPORTED_METHODS = ["POST"] # type: ignore
+
+ def __init__(
+ self,
+ application: tornado.web.Application,
+ request: httputil.HTTPServerRequest,
+ **kwargs: JSONDict,
+ ):
+ super().__init__(application, request, **kwargs)
+ self.logger = logging.getLogger(__name__)
+
+ def initialize(self, bot: 'Bot', update_queue: Queue) -> None:
+ # pylint: disable=W0201
+ self.bot = bot
+ self.update_queue = update_queue
+
+ def set_default_headers(self) -> None:
+ self.set_header("Content-Type", 'application/json; charset="utf-8"')
+
+ def post(self) -> None:
+ self.logger.debug('Webhook triggered')
+ self._validate_post()
+ json_string = self.request.body.decode()
+ data = json.loads(json_string)
+ self.set_status(200)
+ self.logger.debug('Webhook received data: %s', json_string)
+ update = Update.de_json(data, self.bot)
+ if update:
+ self.logger.debug('Received Update with ID %d on Webhook', update.update_id)
+ # handle arbitrary callback data, if necessary
+ if isinstance(self.bot, ExtBot):
+ self.bot.insert_callback_data(update)
+ self.update_queue.put(update)
+
+ def _validate_post(self) -> None:
+ ct_header = self.request.headers.get("Content-Type", None)
+ if ct_header != 'application/json':
+ raise tornado.web.HTTPError(403)
+
+ def write_error(self, status_code: int, **kwargs: Any) -> None:
+ """Log an arbitrary message.
+
+ This is used by all other logging functions.
+
+ It overrides ``BaseHTTPRequestHandler.log_message``, which logs to ``sys.stderr``.
+
+ The first argument, FORMAT, is a format string for the message to be logged. If the format
+ string contains any % escapes requiring parameters, they should be specified as subsequent
+ arguments (it's just like printf!).
+
+ The client ip is prefixed to every message.
+
+ """
+ super().write_error(status_code, **kwargs)
+ self.logger.debug(
+ "%s - - %s",
+ self.request.remote_ip,
+ "Exception in WebhookHandler",
+ exc_info=kwargs['exc_info'],
+ )
diff --git a/telegramer/include/telegram/files/animation.py b/telegramer/include/telegram/files/animation.py
index 4ff6594..a9f2ab5 100644
--- a/telegramer/include/telegram/files/animation.py
+++ b/telegramer/include/telegram/files/animation.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,26 +17,28 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Animation."""
-from telegram import PhotoSize
-from telegram import TelegramObject
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import PhotoSize, TelegramObject
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File
class Animation(TelegramObject):
- """This object represents an animation file to be displayed in the message containing a game.
+ """This object represents an animation file (GIF or H.264/MPEG-4 AVC video without sound).
- Attributes:
- file_id (:obj:`str`): Unique file identifier.
- width (:obj:`int`): Video width as defined by sender.
- height (:obj:`int`): Video height as defined by sender.
- duration (:obj:`int`): Duration of the video in seconds as defined by sender.
- thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined
- by sender.
- file_name (:obj:`str`): Optional. Original animation filename as defined by sender.
- mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
- file_size (:obj:`int`): Optional. File size.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
Args:
- file_id (:obj:`str`): Unique file identifier.
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
width (:obj:`int`): Video width as defined by sender.
height (:obj:`int`): Video height as defined by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
@@ -44,38 +46,92 @@ class Animation(TelegramObject):
file_name (:obj:`str`, optional): Original animation filename as defined by sender.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
file_size (:obj:`int`, optional): File size.
+ bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
+ Attributes:
+ file_id (:obj:`str`): File identifier.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ width (:obj:`int`): Video width as defined by sender.
+ height (:obj:`int`): Video height as defined by sender.
+ duration (:obj:`int`): Duration of the video in seconds as defined by sender.
+ thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined by sender.
+ file_name (:obj:`str`): Optional. Original animation filename as defined by sender.
+ mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
+ file_size (:obj:`int`): Optional. File size.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
"""
- def __init__(self,
- file_id,
- width,
- height,
- duration,
- thumb=None,
- file_name=None,
- mime_type=None,
- file_size=None,
- **kwargs):
+ __slots__ = (
+ 'bot',
+ 'width',
+ 'file_id',
+ 'file_size',
+ 'file_name',
+ 'thumb',
+ 'duration',
+ 'mime_type',
+ 'height',
+ 'file_unique_id',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ width: int,
+ height: int,
+ duration: int,
+ thumb: PhotoSize = None,
+ file_name: str = None,
+ mime_type: str = None,
+ file_size: int = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = str(file_id)
+ self.file_unique_id = str(file_unique_id)
self.width = int(width)
self.height = int(height)
self.duration = duration
+ # Optionals
self.thumb = thumb
self.file_name = file_name
self.mime_type = mime_type
self.file_size = file_size
+ self.bot = bot
- self._id_attrs = (self.file_id,)
+ self._id_attrs = (self.file_unique_id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Animation']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(Animation, cls).de_json(data, bot)
-
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
- return cls(**data)
+ return cls(bot=bot, **data)
+
+ def get_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
+ """Convenience wrapper over :attr:`telegram.Bot.get_file`
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
+
+ Returns:
+ :class:`telegram.File`
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs)
diff --git a/telegramer/include/telegram/files/audio.py b/telegramer/include/telegram/files/audio.py
index b743669..af6683c 100644
--- a/telegramer/include/telegram/files/audio.py
+++ b/telegramer/include/telegram/files/audio.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,64 +18,105 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Audio."""
-from telegram import TelegramObject, PhotoSize
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import PhotoSize, TelegramObject
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File
class Audio(TelegramObject):
"""This object represents an audio file to be treated as music by the Telegram clients.
- Attributes:
- file_id (:obj:`str`): Unique identifier for this file.
- duration (:obj:`int`): Duration of the audio in seconds.
- performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio
- tags.
- title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags.
- mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
- file_size (:obj:`int`): Optional. File size.
- thumb (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to
- which the music file belongs
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
Args:
- file_id (:obj:`str`): Unique identifier for this file.
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be
+ the same over time and for different bots. Can't be used to download or reuse the file.
duration (:obj:`int`): Duration of the audio in seconds as defined by sender.
performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio
tags.
title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags.
+ file_name (:obj:`str`, optional): Original filename as defined by sender.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
file_size (:obj:`int`, optional): File size.
thumb (:class:`telegram.PhotoSize`, optional): Thumbnail of the album cover to
- which the music file belongs
+ which the music file belongs.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ file_id (:obj:`str`): Identifier for this file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ duration (:obj:`int`): Duration of the audio in seconds.
+ performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio
+ tags.
+ title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags.
+ file_name (:obj:`str`): Optional. Original filename as defined by sender.
+ mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
+ file_size (:obj:`int`): Optional. File size.
+ thumb (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to
+ which the music file belongs.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+
"""
- def __init__(self,
- file_id,
- duration,
- performer=None,
- title=None,
- mime_type=None,
- file_size=None,
- thumb=None,
- bot=None,
- **kwargs):
+ __slots__ = (
+ 'file_id',
+ 'bot',
+ 'file_size',
+ 'file_name',
+ 'thumb',
+ 'title',
+ 'duration',
+ 'performer',
+ 'mime_type',
+ 'file_unique_id',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ duration: int,
+ performer: str = None,
+ title: str = None,
+ mime_type: str = None,
+ file_size: int = None,
+ thumb: PhotoSize = None,
+ bot: 'Bot' = None,
+ file_name: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = str(file_id)
+ self.file_unique_id = str(file_unique_id)
self.duration = int(duration)
# Optionals
self.performer = performer
self.title = title
+ self.file_name = file_name
self.mime_type = mime_type
self.file_size = file_size
self.thumb = thumb
self.bot = bot
- self._id_attrs = (self.file_id,)
+ self._id_attrs = (self.file_unique_id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Audio']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
@@ -83,20 +124,18 @@ def de_json(cls, data, bot):
return cls(bot=bot, **data)
- def get_file(self, timeout=None, **kwargs):
+ def get_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
- Args:
- timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout from the server (instead of the one specified during creation of
- the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
Returns:
:class:`telegram.File`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
+ return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs)
diff --git a/telegramer/include/telegram/files/chatphoto.py b/telegramer/include/telegram/files/chatphoto.py
index a3f5171..2f55bf9 100644
--- a/telegramer/include/telegram/files/chatphoto.py
+++ b/telegramer/include/telegram/files/chatphoto.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,36 +17,116 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatPhoto."""
-
-# TODO: add direct download shortcuts.
+from typing import TYPE_CHECKING, Any
from telegram import TelegramObject
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File
class ChatPhoto(TelegramObject):
"""This object represents a chat photo.
- Attributes:
- small_file_id (:obj:`str`): Unique file identifier of small (160x160) chat photo.
- big_file_id (:obj:`str`): Unique file identifier of big (640x640) chat photo.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`small_file_unique_id` and :attr:`big_file_unique_id` are
+ equal.
Args:
small_file_id (:obj:`str`): Unique file identifier of small (160x160) chat photo. This
- file_id can be used only for photo download.
+ file_id can be used only for photo download and only for as long
+ as the photo is not changed.
+ small_file_unique_id (:obj:`str`): Unique file identifier of small (160x160) chat photo,
+ which is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
big_file_id (:obj:`str`): Unique file identifier of big (640x640) chat photo. This file_id
- can be used only for photo download.
+ can be used only for photo download and only for as long as the photo is not changed.
+ big_file_unique_id (:obj:`str`): Unique file identifier of big (640x640) chat photo,
+ which is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ small_file_id (:obj:`str`): File identifier of small (160x160) chat photo.
+ This file_id can be used only for photo download and only for as long
+ as the photo is not changed.
+ small_file_unique_id (:obj:`str`): Unique file identifier of small (160x160) chat photo,
+ which is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ big_file_id (:obj:`str`): File identifier of big (640x640) chat photo.
+ This file_id can be used only for photo download and only for as long as
+ the photo is not changed.
+ big_file_unique_id (:obj:`str`): Unique file identifier of big (640x640) chat photo,
+ which is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+
"""
- def __init__(self, small_file_id, big_file_id, bot=None, **kwargs):
+ __slots__ = (
+ 'big_file_unique_id',
+ 'bot',
+ 'small_file_id',
+ 'small_file_unique_id',
+ 'big_file_id',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ small_file_id: str,
+ small_file_unique_id: str,
+ big_file_id: str,
+ big_file_unique_id: str,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
self.small_file_id = small_file_id
+ self.small_file_unique_id = small_file_unique_id
self.big_file_id = big_file_id
+ self.big_file_unique_id = big_file_unique_id
+
+ self.bot = bot
+
+ self._id_attrs = (
+ self.small_file_unique_id,
+ self.big_file_unique_id,
+ )
+
+ def get_small_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
+ """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the
+ small (160x160) chat photo
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
+
+ Returns:
+ :class:`telegram.File`
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ return self.bot.get_file(
+ file_id=self.small_file_id, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def get_big_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
+ """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the
+ big (640x640) chat photo
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
+
+ Returns:
+ :class:`telegram.File`
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
+ Raises:
+ :class:`telegram.error.TelegramError`
- return cls(bot=bot, **data)
+ """
+ return self.bot.get_file(file_id=self.big_file_id, timeout=timeout, api_kwargs=api_kwargs)
diff --git a/telegramer/include/telegram/files/contact.py b/telegramer/include/telegram/files/contact.py
index e3cd88c..cee769f 100644
--- a/telegramer/include/telegram/files/contact.py
+++ b/telegramer/include/telegram/files/contact.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,18 +18,16 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Contact."""
+from typing import Any
+
from telegram import TelegramObject
class Contact(TelegramObject):
"""This object represents a phone contact.
- Attributes:
- phone_number (:obj:`str`): Contact's phone number.
- first_name (:obj:`str`): Contact's first name.
- last_name (:obj:`str`): Optional. Contact's last name.
- user_id (:obj:`int`): Optional. Contact's user identifier in Telegram.
- vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`phone_number` is equal.
Args:
phone_number (:obj:`str`): Contact's phone number.
@@ -39,10 +37,26 @@ class Contact(TelegramObject):
vcard (:obj:`str`, optional): Additional data about the contact in the form of a vCard.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ phone_number (:obj:`str`): Contact's phone number.
+ first_name (:obj:`str`): Contact's first name.
+ last_name (:obj:`str`): Optional. Contact's last name.
+ user_id (:obj:`int`): Optional. Contact's user identifier in Telegram.
+ vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard.
+
"""
- def __init__(self, phone_number, first_name, last_name=None, user_id=None, vcard=None,
- **kwargs):
+ __slots__ = ('vcard', 'user_id', 'first_name', 'last_name', 'phone_number', '_id_attrs')
+
+ def __init__(
+ self,
+ phone_number: str,
+ first_name: str,
+ last_name: str = None,
+ user_id: int = None,
+ vcard: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.phone_number = str(phone_number)
self.first_name = first_name
@@ -52,10 +66,3 @@ def __init__(self, phone_number, first_name, last_name=None, user_id=None, vcard
self.vcard = vcard
self._id_attrs = (self.phone_number,)
-
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- return cls(**data)
diff --git a/telegramer/include/telegram/files/document.py b/telegramer/include/telegram/files/document.py
index f23940c..a0ffcef 100644
--- a/telegramer/include/telegram/files/document.py
+++ b/telegramer/include/telegram/files/document.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,22 +18,28 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Document."""
+from typing import TYPE_CHECKING, Any, Optional
+
from telegram import PhotoSize, TelegramObject
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File
class Document(TelegramObject):
- """This object represents a general file (as opposed to photos, voice messages and audio files).
+ """This object represents a general file
+ (as opposed to photos, voice messages and audio files).
- Attributes:
- file_id (:obj:`str`): Unique file identifier.
- thumb (:class:`telegram.PhotoSize`): Optional. Document thumbnail.
- file_name (:obj:`str`): Original filename.
- mime_type (:obj:`str`): Optional. MIME type of the file.
- file_size (:obj:`int`): Optional. File size.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
Args:
- file_id (:obj:`str`): Unique file identifier
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be
+ the same over time and for different bots. Can't be used to download or reuse the file.
thumb (:class:`telegram.PhotoSize`, optional): Document thumbnail as defined by sender.
file_name (:obj:`str`, optional): Original filename as defined by sender.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
@@ -41,19 +47,46 @@ class Document(TelegramObject):
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ file_id (:obj:`str`): File identifier.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ thumb (:class:`telegram.PhotoSize`): Optional. Document thumbnail.
+ file_name (:obj:`str`): Original filename.
+ mime_type (:obj:`str`): Optional. MIME type of the file.
+ file_size (:obj:`int`): Optional. File size.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+
"""
+
+ __slots__ = (
+ 'bot',
+ 'file_id',
+ 'file_size',
+ 'file_name',
+ 'thumb',
+ 'mime_type',
+ 'file_unique_id',
+ '_id_attrs',
+ )
+
_id_keys = ('file_id',)
- def __init__(self,
- file_id,
- thumb=None,
- file_name=None,
- mime_type=None,
- file_size=None,
- bot=None,
- **kwargs):
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ thumb: PhotoSize = None,
+ file_name: str = None,
+ mime_type: str = None,
+ file_size: int = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = str(file_id)
+ self.file_unique_id = str(file_unique_id)
# Optionals
self.thumb = thumb
self.file_name = file_name
@@ -61,33 +94,32 @@ def __init__(self,
self.file_size = file_size
self.bot = bot
- self._id_attrs = (self.file_id,)
+ self._id_attrs = (self.file_unique_id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Document']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(Document, cls).de_json(data, bot)
-
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
return cls(bot=bot, **data)
- def get_file(self, timeout=None, **kwargs):
+ def get_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
- Args:
- timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout from the server (instead of the one specified during creation of
- the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
Returns:
:class:`telegram.File`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
+ return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs)
diff --git a/telegramer/include/telegram/files/file.py b/telegramer/include/telegram/files/file.py
index 7e35235..ca77f9d 100644
--- a/telegramer/include/telegram/files/file.py
+++ b/telegramer/include/telegram/files/file.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,72 +17,101 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram File."""
+import os
+import shutil
+import urllib.parse as urllib_parse
from base64 import b64decode
from os.path import basename
-
-# REMREM from future.backports.urllib import parse as urllib_parse
-from urlparse import urlparse as urllib_parse
+from typing import IO, TYPE_CHECKING, Any, Optional, Union
from telegram import TelegramObject
from telegram.passport.credentials import decrypt
+from telegram.utils.helpers import is_local_file
+
+if TYPE_CHECKING:
+ from telegram import Bot, FileCredentials
class File(TelegramObject):
"""
This object represents a file ready to be downloaded. The file can be downloaded with
:attr:`download`. It is guaranteed that the link will be valid for at least 1 hour. When the
- link expires, a new one can be requested by calling getFile.
+ link expires, a new one can be requested by calling :meth:`telegram.Bot.get_file`.
- Note:
- Maximum file size to download is 20 MB
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
- Attributes:
- file_id (:obj:`str`): Unique identifier for this file.
- file_size (:obj:`str`): Optional. File size.
- file_path (:obj:`str`): Optional. File path. Use :attr:`download` to get the file.
+ Note:
+ * Maximum file size to download is 20 MB.
+ * If you obtain an instance of this class from :attr:`telegram.PassportFile.get_file`,
+ then it will automatically be decrypted as it downloads when you call :attr:`download()`.
Args:
- file_id (:obj:`str`): Unique identifier for this file.
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
file_size (:obj:`int`, optional): Optional. File size, if known.
file_path (:obj:`str`, optional): File path. Use :attr:`download` to get the file.
bot (:obj:`telegram.Bot`, optional): Bot to use with shortcut method.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Note:
- If you obtain an instance of this class from :attr:`telegram.PassportFile.get_file`,
- then it will automatically be decrypted as it downloads when you call :attr:`download()`.
+ Attributes:
+ file_id (:obj:`str`): Identifier for this file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ file_size (:obj:`str`): Optional. File size.
+ file_path (:obj:`str`): Optional. File path. Use :attr:`download` to get the file.
"""
- def __init__(self, file_id, bot=None, file_size=None, file_path=None, **kwargs):
+ __slots__ = (
+ 'bot',
+ 'file_id',
+ 'file_size',
+ 'file_unique_id',
+ 'file_path',
+ '_credentials',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ bot: 'Bot' = None,
+ file_size: int = None,
+ file_path: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = str(file_id)
-
+ self.file_unique_id = str(file_unique_id)
# Optionals
self.file_size = file_size
self.file_path = file_path
-
self.bot = bot
- self._credentials = None
+ self._credentials: Optional['FileCredentials'] = None
- self._id_attrs = (self.file_id,)
+ self._id_attrs = (self.file_unique_id,)
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- return cls(bot=bot, **data)
-
- def download(self, custom_path=None, out=None, timeout=None):
+ def download(
+ self, custom_path: str = None, out: IO = None, timeout: int = None
+ ) -> Union[str, IO]:
"""
Download this file. By default, the file is saved in the current working directory with its
- original filename as reported by Telegram. If a :attr:`custom_path` is supplied, it will be
- saved to that path instead. If :attr:`out` is defined, the file contents will be saved to
- that object using the ``out.write`` method.
+ original filename as reported by Telegram. If the file has no filename, it the file ID will
+ be used as filename. If a :attr:`custom_path` is supplied, it will be saved to that path
+ instead. If :attr:`out` is defined, the file contents will be saved to that object using
+ the ``out.write`` method.
Note:
- :attr:`custom_path` and :attr:`out` are mutually exclusive.
+ * :attr:`custom_path` and :attr:`out` are mutually exclusive.
+ * If neither :attr:`custom_path` nor :attr:`out` is provided and :attr:`file_path` is
+ the path of a local file (which is the case when a Bot API Server is running in
+ local mode), this method will just return the path.
Args:
custom_path (:obj:`str`, optional): Custom path.
@@ -94,7 +123,7 @@ def download(self, custom_path=None, out=None, timeout=None):
Returns:
:obj:`str` | :obj:`io.BufferedWriter`: The same object as :attr:`out` if specified.
- Otherwise, returns the filename downloaded to.
+ Otherwise, returns the filename downloaded to or the file path of the local file.
Raises:
ValueError: If both :attr:`custom_path` and :attr:`out` are passed.
@@ -103,39 +132,59 @@ def download(self, custom_path=None, out=None, timeout=None):
if custom_path is not None and out is not None:
raise ValueError('custom_path and out are mutually exclusive')
- # Convert any UTF-8 char into a url encoded ASCII string.
- url = self._get_encoded_url()
+ local_file = is_local_file(self.file_path)
+
+ if local_file:
+ url = self.file_path
+ else:
+ # Convert any UTF-8 char into a url encoded ASCII string.
+ url = self._get_encoded_url()
if out:
- buf = self.bot.request.retrieve(url)
- if self._credentials:
- buf = decrypt(b64decode(self._credentials.secret),
- b64decode(self._credentials.hash),
- buf)
+ if local_file:
+ with open(url, 'rb') as file:
+ buf = file.read()
+ else:
+ buf = self.bot.request.retrieve(url)
+ if self._credentials:
+ buf = decrypt(
+ b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf
+ )
out.write(buf)
return out
+
+ if custom_path and local_file:
+ shutil.copyfile(self.file_path, custom_path)
+ return custom_path
+
+ if custom_path:
+ filename = custom_path
+ elif local_file:
+ return self.file_path
+ elif self.file_path:
+ filename = basename(self.file_path)
else:
- if custom_path:
- filename = custom_path
- else:
- filename = basename(self.file_path)
-
- buf = self.bot.request.retrieve(url, timeout=timeout)
- if self._credentials:
- buf = decrypt(b64decode(self._credentials.secret),
- b64decode(self._credentials.hash),
- buf)
- with open(filename, 'wb') as fobj:
- fobj.write(buf)
- return filename
-
- def _get_encoded_url(self):
+ filename = os.path.join(os.getcwd(), self.file_id)
+
+ buf = self.bot.request.retrieve(url, timeout=timeout)
+ if self._credentials:
+ buf = decrypt(
+ b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf
+ )
+ with open(filename, 'wb') as fobj:
+ fobj.write(buf)
+ return filename
+
+ def _get_encoded_url(self) -> str:
"""Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string."""
sres = urllib_parse.urlsplit(self.file_path)
- return urllib_parse.urlunsplit(urllib_parse.SplitResult(
- sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment))
+ return urllib_parse.urlunsplit(
+ urllib_parse.SplitResult(
+ sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment
+ )
+ )
- def download_as_bytearray(self, buf=None):
+ def download_as_bytearray(self, buf: bytearray = None) -> bytes:
"""Download this file and return it as a bytearray.
Args:
@@ -148,9 +197,17 @@ def download_as_bytearray(self, buf=None):
"""
if buf is None:
buf = bytearray()
-
- buf.extend(self.bot.request.retrieve(self._get_encoded_url()))
+ if is_local_file(self.file_path):
+ with open(self.file_path, "rb") as file:
+ buf.extend(file.read())
+ else:
+ buf.extend(self.bot.request.retrieve(self._get_encoded_url()))
return buf
- def set_credentials(self, credentials):
+ def set_credentials(self, credentials: 'FileCredentials') -> None:
+ """Sets the passport credentials for the file.
+
+ Args:
+ credentials (:class:`telegram.FileCredentials`): The credentials.
+ """
self._credentials = credentials
diff --git a/telegramer/include/telegram/files/inputfile.py b/telegramer/include/telegram/files/inputfile.py
index a328ad8..2c3196f 100644
--- a/telegramer/include/telegram/files/inputfile.py
+++ b/telegramer/include/telegram/files/inputfile.py
@@ -2,7 +2,7 @@
# pylint: disable=W0622,E0611
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -20,26 +20,24 @@
"""This module contains an object that represents a Telegram InputFile."""
import imghdr
+import logging
import mimetypes
import os
-import sys
+from typing import IO, Optional, Tuple, Union
from uuid import uuid4
-from telegram import TelegramError
+from telegram.utils.deprecate import set_new_attribute_deprecated
DEFAULT_MIME_TYPE = 'application/octet-stream'
+logger = logging.getLogger(__name__)
-class InputFile(object):
+class InputFile:
"""This object represents a Telegram InputFile.
- Attributes:
- input_file_content (:obj:`bytes`): The binaray content of the file to send.
- filename (:obj:`str`): Optional, Filename for the file to be sent.
- attach (:obj:`str`): Optional, attach id for sending multiple files.
-
Args:
- obj (:obj:`File handler`): An open file descriptor.
+ obj (:obj:`File handler` | :obj:`bytes`): An open file descriptor or the files content as
+ bytes.
filename (:obj:`str`, optional): Filename for this InputFile.
attach (:obj:`bool`, optional): Whether this should be send as one file or is part of a
collection of files.
@@ -47,62 +45,75 @@ class InputFile(object):
Raises:
TelegramError
+ Attributes:
+ input_file_content (:obj:`bytes`): The binary content of the file to send.
+ filename (:obj:`str`): Optional. Filename for the file to be sent.
+ attach (:obj:`str`): Optional. Attach id for sending multiple files.
+
"""
- def __init__(self, obj, filename=None, attach=None):
+ __slots__ = ('filename', 'attach', 'input_file_content', 'mimetype', '__dict__')
+
+ def __init__(self, obj: Union[IO, bytes], filename: str = None, attach: bool = None):
self.filename = None
- self.input_file_content = obj.read()
+ if isinstance(obj, bytes):
+ self.input_file_content = obj
+ else:
+ self.input_file_content = obj.read()
self.attach = 'attached' + uuid4().hex if attach else None
if filename:
self.filename = filename
- elif (hasattr(obj, 'name') and
- not isinstance(obj.name, int) and # py3
- obj.name != ''): # py2
- # on py2.7, pylint fails to understand this properly
- # pylint: disable=E1101
- self.filename = os.path.basename(obj.name)
-
- try:
- self.mimetype = self.is_image(self.input_file_content)
- except TelegramError:
- if self.filename:
- self.mimetype = mimetypes.guess_type(
- self.filename)[0] or DEFAULT_MIME_TYPE
- else:
- self.mimetype = DEFAULT_MIME_TYPE
- if not self.filename or '.' not in self.filename:
+ elif hasattr(obj, 'name') and not isinstance(obj.name, int): # type: ignore[union-attr]
+ self.filename = os.path.basename(obj.name) # type: ignore[union-attr]
+
+ image_mime_type = self.is_image(self.input_file_content)
+ if image_mime_type:
+ self.mimetype = image_mime_type
+ elif self.filename:
+ self.mimetype = mimetypes.guess_type(self.filename)[0] or DEFAULT_MIME_TYPE
+ else:
+ self.mimetype = DEFAULT_MIME_TYPE
+
+ if not self.filename:
self.filename = self.mimetype.replace('/', '.')
- if sys.version_info < (3,):
- if isinstance(self.filename, unicode): # flake8: noqa pylint: disable=E0602
- self.filename = self.filename.encode('utf-8', 'replace')
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
@property
- def field_tuple(self):
+ def field_tuple(self) -> Tuple[str, bytes, str]: # skipcq: PY-D0003
return self.filename, self.input_file_content, self.mimetype
@staticmethod
- def is_image(stream):
+ def is_image(stream: bytes) -> Optional[str]:
"""Check if the content file is an image by analyzing its headers.
Args:
- stream (:obj:`str`): A str representing the content of a file.
+ stream (:obj:`bytes`): A byte stream representing the content of a file.
Returns:
- :obj:`str`: The str mime-type of an image.
+ :obj:`str` | :obj:`None`: The mime-type of an image, if the input is an image, or
+ :obj:`None` else.
"""
- image = imghdr.what(None, stream)
- if image:
- return 'image/%s' % image
-
- raise TelegramError('Could not parse file content')
+ try:
+ image = imghdr.what(None, stream)
+ if image:
+ return f'image/{image}'
+ return None
+ except Exception:
+ logger.debug(
+ "Could not parse file content. Assuming that file is not an image.", exc_info=True
+ )
+ return None
@staticmethod
- def is_file(obj):
+ def is_file(obj: object) -> bool: # skipcq: PY-D0003
return hasattr(obj, 'read')
- def to_dict(self):
+ def to_dict(self) -> Optional[str]:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
if self.attach:
return 'attach://' + self.attach
+ return None
diff --git a/telegramer/include/telegram/files/inputmedia.py b/telegramer/include/telegram/files/inputmedia.py
index b639872..c4899fb 100644
--- a/telegramer/include/telegram/files/inputmedia.py
+++ b/telegramer/include/telegram/files/inputmedia.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,20 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram InputMedia Objects."""
-from telegram import TelegramObject, InputFile, PhotoSize, Animation, Video, Audio, Document
+from typing import Union, List, Tuple
+
+from telegram import (
+ Animation,
+ Audio,
+ Document,
+ InputFile,
+ PhotoSize,
+ TelegramObject,
+ Video,
+ MessageEntity,
+)
+from telegram.utils.helpers import DEFAULT_NONE, parse_file_input
+from telegram.utils.types import FileInput, JSONDict, ODVInput
class InputMedia(TelegramObject):
@@ -29,75 +42,119 @@ class InputMedia(TelegramObject):
:class:`telegram.InputMediaVideo` for detailed use.
"""
- pass
+
+ __slots__ = ()
+ caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...], None] = None
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
+
+ if self.caption_entities:
+ data['caption_entities'] = [
+ ce.to_dict() for ce in self.caption_entities # pylint: disable=E1133
+ ]
+
+ return data
class InputMediaAnimation(InputMedia):
"""Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
- Attributes:
- type (:obj:`str`): ``animation``.
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
- servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
- Lastly you can pass an existing :class:`telegram.Animation` object to send.
- thumb (`filelike object`): Optional. Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
- caption (:obj:`str`): Optional. Caption of the animation to be sent, 0-200 characters.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- width (:obj:`int`): Optional. Animation width.
- height (:obj:`int`): Optional. Animation height.
- duration (:obj:`int`): Optional. Animation duration.
-
+ Note:
+ When using a :class:`telegram.Animation` for the :attr:`media` attribute. It will take the
+ width, height and duration from that video, unless otherwise specified with the optional
+ arguments.
Args:
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
- servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
- Lastly you can pass an existing :class:`telegram.Animation` object to send.
- thumb (`filelike object`, optional): Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
- caption (:obj:`str`, optional): Caption of the animation to be sent, 0-200 characters.
+ media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Animation`): File to send. Pass a
+ file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
+ URL for Telegram to get a file from the Internet. Lastly you can pass an existing
+ :class:`telegram.Animation` object to send.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the animation, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
+ thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of
+ the file sent; can be ignored if
+ thumbnail generation for the file is supported server-side. The thumbnail should be
+ in JPEG format and less than 200 kB in size. A thumbnail's width and height should
+ not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
+ Thumbnails can't be reused and can be only uploaded as a new file.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ caption (:obj:`str`, optional): Caption of the animation to be sent, 0-1024 characters
+ after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of parse_mode.
width (:obj:`int`, optional): Animation width.
height (:obj:`int`, optional): Animation height.
duration (:obj:`int`, optional): Animation duration.
- Note:
- When using a :class:`telegram.Animation` for the :attr:`media` attribute. It will take the
- width, height and duration from that video, unless otherwise specified with the optional
- arguments.
+ Attributes:
+ type (:obj:`str`): ``animation``.
+ media (:obj:`str` | :class:`telegram.InputFile`): Animation to send.
+ caption (:obj:`str`): Optional. Caption of the document to be sent.
+ parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption.
+ thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
+ width (:obj:`int`): Optional. Animation width.
+ height (:obj:`int`): Optional. Animation height.
+ duration (:obj:`int`): Optional. Animation duration.
+
"""
- def __init__(self, media, thumb=None, caption=None, parse_mode=None, width=None, height=None,
- duration=None):
+ __slots__ = (
+ 'caption_entities',
+ 'width',
+ 'media',
+ 'thumb',
+ 'caption',
+ 'duration',
+ 'parse_mode',
+ 'height',
+ 'type',
+ )
+
+ def __init__(
+ self,
+ media: Union[FileInput, Animation],
+ thumb: FileInput = None,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ width: int = None,
+ height: int = None,
+ duration: int = None,
+ caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+ filename: str = None,
+ ):
self.type = 'animation'
if isinstance(media, Animation):
- self.media = media.file_id
+ self.media: Union[str, InputFile] = media.file_id
self.width = media.width
self.height = media.height
self.duration = media.duration
- elif InputFile.is_file(media):
- self.media = InputFile(media, attach=True)
else:
- self.media = media
+ self.media = parse_file_input(media, attach=True, filename=filename)
if thumb:
- self.thumb = thumb
- if InputFile.is_file(self.thumb):
- self.thumb = InputFile(self.thumb, attach=True)
+ self.thumb = parse_file_input(thumb, attach=True)
if caption:
self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
if width:
self.width = width
if height:
@@ -109,111 +166,163 @@ def __init__(self, media, thumb=None, caption=None, parse_mode=None, width=None,
class InputMediaPhoto(InputMedia):
"""Represents a photo to be sent.
- Attributes:
- type (:obj:`str`): ``photo``.
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the
- Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the
- Internet. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
- caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-200 characters.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
-
Args:
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the
- Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the
- Internet. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
- caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-200 characters.
+ media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.PhotoSize`): File to send. Pass a
+ file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
+ URL for Telegram to get a file from the Internet. Lastly you can pass an existing
+ :class:`telegram.PhotoSize` object to send.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the photo, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
+ caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-1024 characters after
+ entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of parse_mode.
+
+ Attributes:
+ type (:obj:`str`): ``photo``.
+ media (:obj:`str` | :class:`telegram.InputFile`): Photo to send.
+ caption (:obj:`str`): Optional. Caption of the document to be sent.
+ parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption.
+
"""
- def __init__(self, media, caption=None, parse_mode=None):
- self.type = 'photo'
+ __slots__ = ('caption_entities', 'media', 'caption', 'parse_mode', 'type')
- if isinstance(media, PhotoSize):
- self.media = media.file_id
- elif InputFile.is_file(media):
- self.media = InputFile(media, attach=True)
- else:
- self.media = media
+ def __init__(
+ self,
+ media: Union[FileInput, PhotoSize],
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+ filename: str = None,
+ ):
+ self.type = 'photo'
+ self.media = parse_file_input(media, PhotoSize, attach=True, filename=filename)
if caption:
self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
class InputMediaVideo(InputMedia):
"""Represents a video to be sent.
- Attributes:
- type (:obj:`str`): ``video``.
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
- servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
- Lastly you can pass an existing :class:`telegram.Video` object to send.
- caption (:obj:`str`): Optional. Caption of the video to be sent, 0-200 characters.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- width (:obj:`int`): Optional. Video width.
- height (:obj:`int`): Optional. Video height.
- duration (:obj:`int`): Optional. Video duration.
- supports_streaming (:obj:`bool`): Optional. Pass True, if the uploaded video is suitable
- for streaming.
- thumb (`filelike object`): Optional. Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ Note:
+ * When using a :class:`telegram.Video` for the :attr:`media` attribute. It will take the
+ width, height and duration from that video, unless otherwise specified with the optional
+ arguments.
+ * ``thumb`` will be ignored for small video files, for which Telegram can easily
+ generate thumb nails. However, this behaviour is undocumented and might be changed
+ by Telegram.
Args:
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
- servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
- Lastly you can pass an existing :class:`telegram.Video` object to send.
- caption (:obj:`str`, optional): Caption of the video to be sent, 0-200 characters.
+ media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Video`): File to send. Pass a
+ file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
+ URL for Telegram to get a file from the Internet. Lastly you can pass an existing
+ :class:`telegram.Video` object to send.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the video, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
+ caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters after
+ entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of parse_mode.
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
duration (:obj:`int`, optional): Video duration.
- supports_streaming (:obj:`bool`, optional): Pass True, if the uploaded video is suitable
- for streaming.
- thumb (`filelike object`, optional): Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is
+ suitable for streaming.
+ thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of
+ the file sent; can be ignored if
+ thumbnail generation for the file is supported server-side. The thumbnail should be
+ in JPEG format and less than 200 kB in size. A thumbnail's width and height should
+ not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
+ Thumbnails can't be reused and can be only uploaded as a new file.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+
+ Attributes:
+ type (:obj:`str`): ``video``.
+ media (:obj:`str` | :class:`telegram.InputFile`): Video file to send.
+ caption (:obj:`str`): Optional. Caption of the document to be sent.
+ parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption.
+ width (:obj:`int`): Optional. Video width.
+ height (:obj:`int`): Optional. Video height.
+ duration (:obj:`int`): Optional. Video duration.
+ supports_streaming (:obj:`bool`): Optional. Pass :obj:`True`, if the uploaded video is
+ suitable for streaming.
+ thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
- Note:
- When using a :class:`telegram.Video` for the :attr:`media` attribute. It will take the
- width, height and duration from that video, unless otherwise specified with the optional
- arguments.
"""
- def __init__(self, media, caption=None, width=None, height=None, duration=None,
- supports_streaming=None, parse_mode=None, thumb=None):
+ __slots__ = (
+ 'caption_entities',
+ 'width',
+ 'media',
+ 'thumb',
+ 'supports_streaming',
+ 'caption',
+ 'duration',
+ 'parse_mode',
+ 'height',
+ 'type',
+ )
+
+ def __init__(
+ self,
+ media: Union[FileInput, Video],
+ caption: str = None,
+ width: int = None,
+ height: int = None,
+ duration: int = None,
+ supports_streaming: bool = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb: FileInput = None,
+ caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+ filename: str = None,
+ ):
self.type = 'video'
if isinstance(media, Video):
- self.media = media.file_id
+ self.media: Union[str, InputFile] = media.file_id
self.width = media.width
self.height = media.height
self.duration = media.duration
- elif InputFile.is_file(media):
- self.media = InputFile(media, attach=True)
else:
- self.media = media
+ self.media = parse_file_input(media, attach=True, filename=filename)
if thumb:
- self.thumb = thumb
- if InputFile.is_file(self.thumb):
- self.thumb = InputFile(self.thumb, attach=True)
+ self.thumb = parse_file_input(thumb, attach=True)
if caption:
self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
if width:
self.width = width
if height:
@@ -227,70 +336,103 @@ def __init__(self, media, caption=None, width=None, height=None, duration=None,
class InputMediaAudio(InputMedia):
"""Represents an audio file to be treated as music to be sent.
- Attributes:
- type (:obj:`str`): ``audio``.
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
- servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
- Lastly you can pass an existing :class:`telegram.Audio` object to send.
- caption (:obj:`str`): Optional. Caption of the audio to be sent, 0-200 characters.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- duration (:obj:`int`): Duration of the audio in seconds.
- performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio
- tags.
- title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags.
- thumb (`filelike object`): Optional. Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ Note:
+ When using a :class:`telegram.Audio` for the :attr:`media` attribute. It will take the
+ duration, performer and title from that video, unless otherwise specified with the
+ optional arguments.
Args:
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
- servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
- Lastly you can pass an existing :class:`telegram.Document` object to send.
- caption (:obj:`str`, optional): Caption of the audio to be sent, 0-200 characters.
+ media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Audio`):
+ File to send. Pass a
+ file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
+ URL for Telegram to get a file from the Internet. Lastly you can pass an existing
+ :class:`telegram.Audio` object to send.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the audio, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
+ caption (:obj:`str`, optional): Caption of the audio to be sent, 0-1024 characters after
+ entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of parse_mode.
duration (:obj:`int`): Duration of the audio in seconds as defined by sender.
performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio
tags.
title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags.
- thumb (`filelike object`, optional): Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of
+ the file sent; can be ignored if
+ thumbnail generation for the file is supported server-side. The thumbnail should be
+ in JPEG format and less than 200 kB in size. A thumbnail's width and height should
+ not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
+ Thumbnails can't be reused and can be only uploaded as a new file.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+
+ Attributes:
+ type (:obj:`str`): ``audio``.
+ media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send.
+ caption (:obj:`str`): Optional. Caption of the document to be sent.
+ parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption.
+ duration (:obj:`int`): Duration of the audio in seconds.
+ performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio
+ tags.
+ title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags.
+ thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
- Note:
- When using a :class:`telegram.Audio` for the :attr:`media` attribute. It will take the
- duration, performer and title from that video, unless otherwise specified with the
- optional arguments.
"""
- def __init__(self, media, thumb=None, caption=None, parse_mode=None,
- duration=None, performer=None, title=None):
+ __slots__ = (
+ 'caption_entities',
+ 'media',
+ 'thumb',
+ 'caption',
+ 'title',
+ 'duration',
+ 'type',
+ 'parse_mode',
+ 'performer',
+ )
+
+ def __init__(
+ self,
+ media: Union[FileInput, Audio],
+ thumb: FileInput = None,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ duration: int = None,
+ performer: str = None,
+ title: str = None,
+ caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+ filename: str = None,
+ ):
self.type = 'audio'
if isinstance(media, Audio):
- self.media = media.file_id
+ self.media: Union[str, InputFile] = media.file_id
self.duration = media.duration
self.performer = media.performer
self.title = media.title
- elif InputFile.is_file(media):
- self.media = InputFile(media, attach=True)
else:
- self.media = media
+ self.media = parse_file_input(media, attach=True, filename=filename)
if thumb:
- self.thumb = thumb
- if InputFile.is_file(self.thumb):
- self.thumb = InputFile(self.thumb, attach=True)
+ self.thumb = parse_file_input(thumb, attach=True)
if caption:
self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
if duration:
self.duration = duration
if performer:
@@ -302,50 +444,82 @@ def __init__(self, media, thumb=None, caption=None, parse_mode=None,
class InputMediaDocument(InputMedia):
"""Represents a general file to be sent.
- Attributes:
- type (:obj:`str`): ``document``.
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
- servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
- Lastly you can pass an existing :class:`telegram.Document` object to send.
- caption (:obj:`str`): Optional. Caption of the document to be sent, 0-200 characters.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- thumb (`filelike object`): Optional. Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
-
Args:
- media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
- servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
- Lastly you can pass an existing :class:`telegram.Document` object to send.
- caption (:obj:`str`, optional): Caption of the document to be sent, 0-200 characters.
+ media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
+ :class:`telegram.Document`): File to send. Pass a
+ file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
+ URL for Telegram to get a file from the Internet. Lastly you can pass an existing
+ :class:`telegram.Document` object to send.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ filename (:obj:`str`, optional): Custom file name for the document, when uploading a
+ new file. Convenience parameter, useful e.g. when sending files generated by the
+ :obj:`tempfile` module.
+
+ .. versionadded:: 13.1
+ caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters after
+ entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- thumb (`filelike object`, optional): Thumbnail of the
- file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
- A thumbnail's width and height should not exceed 90. Ignored if the file is not
- is passed as a string or file_id.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of parse_mode.
+ thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of
+ the file sent; can be ignored if
+ thumbnail generation for the file is supported server-side. The thumbnail should be
+ in JPEG format and less than 200 kB in size. A thumbnail's width and height should
+ not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
+ Thumbnails can't be reused and can be only uploaded as a new file.
+
+ .. versionchanged:: 13.2
+ Accept :obj:`bytes` as input.
+ disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side
+ content type detection for files uploaded using multipart/form-data. Always true, if
+ the document is sent as part of an album.
+
+ Attributes:
+ type (:obj:`str`): ``document``.
+ media (:obj:`str` | :class:`telegram.InputFile`): File to send.
+ caption (:obj:`str`): Optional. Caption of the document to be sent.
+ parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption.
+ thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
+ disable_content_type_detection (:obj:`bool`): Optional. Disables automatic server-side
+ content type detection for files uploaded using multipart/form-data. Always true, if
+ the document is sent as part of an album.
+
"""
- def __init__(self, media, thumb=None, caption=None, parse_mode=None):
+ __slots__ = (
+ 'caption_entities',
+ 'media',
+ 'thumb',
+ 'caption',
+ 'parse_mode',
+ 'type',
+ 'disable_content_type_detection',
+ )
+
+ def __init__(
+ self,
+ media: Union[FileInput, Document],
+ thumb: FileInput = None,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_content_type_detection: bool = None,
+ caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+ filename: str = None,
+ ):
self.type = 'document'
-
- if isinstance(media, Document):
- self.media = media.file_id
- elif InputFile.is_file(media):
- self.media = InputFile(media, attach=True)
- else:
- self.media = media
+ self.media = parse_file_input(media, Document, attach=True, filename=filename)
if thumb:
- self.thumb = thumb
- if InputFile.is_file(self.thumb):
- self.thumb = InputFile(self.thumb, attach=True)
+ self.thumb = parse_file_input(thumb, attach=True)
if caption:
self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.disable_content_type_detection = disable_content_type_detection
diff --git a/telegramer/include/telegram/files/location.py b/telegramer/include/telegram/files/location.py
index f80c8e7..a5f5065 100644
--- a/telegramer/include/telegram/files/location.py
+++ b/telegramer/include/telegram/files/location.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,33 +18,74 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Location."""
+from typing import Any
+
from telegram import TelegramObject
class Location(TelegramObject):
"""This object represents a point on the map.
- Attributes:
- longitude (:obj:`float`): Longitude as defined by sender.
- latitude (:obj:`float`): Latitude as defined by sender.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`longitute` and :attr:`latitude` are equal.
Args:
longitude (:obj:`float`): Longitude as defined by sender.
latitude (:obj:`float`): Latitude as defined by sender.
+ horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location,
+ measured in meters; 0-1500.
+ live_period (:obj:`int`, optional): Time relative to the message sending date, during which
+ the location can be updated, in seconds. For active live locations only.
+ heading (:obj:`int`, optional): The direction in which user is moving, in degrees; 1-360.
+ For active live locations only.
+ proximity_alert_radius (:obj:`int`, optional): Maximum distance for proximity alerts about
+ approaching another chat member, in meters. For sent live locations only.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ longitude (:obj:`float`): Longitude as defined by sender.
+ latitude (:obj:`float`): Latitude as defined by sender.
+ horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location,
+ measured in meters.
+ live_period (:obj:`int`): Optional. Time relative to the message sending date, during which
+ the location can be updated, in seconds. For active live locations only.
+ heading (:obj:`int`): Optional. The direction in which user is moving, in degrees.
+ For active live locations only.
+ proximity_alert_radius (:obj:`int`): Optional. Maximum distance for proximity alerts about
+ approaching another chat member, in meters. For sent live locations only.
+
"""
- def __init__(self, longitude, latitude, **kwargs):
+ __slots__ = (
+ 'longitude',
+ 'horizontal_accuracy',
+ 'proximity_alert_radius',
+ 'live_period',
+ 'latitude',
+ 'heading',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ longitude: float,
+ latitude: float,
+ horizontal_accuracy: float = None,
+ live_period: int = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ **_kwargs: Any,
+ ):
# Required
self.longitude = float(longitude)
self.latitude = float(latitude)
- self._id_attrs = (self.longitude, self.latitude)
-
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
+ # Optionals
+ self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None
+ self.live_period = int(live_period) if live_period else None
+ self.heading = int(heading) if heading else None
+ self.proximity_alert_radius = (
+ int(proximity_alert_radius) if proximity_alert_radius else None
+ )
- return cls(**data)
+ self._id_attrs = (self.longitude, self.latitude)
diff --git a/telegramer/include/telegram/files/photosize.py b/telegramer/include/telegram/files/photosize.py
index 60440a0..9a7988b 100644
--- a/telegramer/include/telegram/files/photosize.py
+++ b/telegramer/include/telegram/files/photosize.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,72 +18,81 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram PhotoSize."""
+from typing import TYPE_CHECKING, Any
+
from telegram import TelegramObject
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File
class PhotoSize(TelegramObject):
"""This object represents one size of a photo or a file/sticker thumbnail.
- Attributes:
- file_id (:obj:`str`): Unique identifier for this file.
- width (:obj:`int`): Photo width.
- height (:obj:`int`): Photo height.
- file_size (:obj:`int`): Optional. File size.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
Args:
- file_id (:obj:`str`): Unique identifier for this file.
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
width (:obj:`int`): Photo width.
height (:obj:`int`): Photo height.
file_size (:obj:`int`, optional): File size.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ file_id (:obj:`str`): Identifier for this file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ width (:obj:`int`): Photo width.
+ height (:obj:`int`): Photo height.
+ file_size (:obj:`int`): Optional. File size.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+
"""
- def __init__(self, file_id, width, height, file_size=None, bot=None, **kwargs):
+ __slots__ = ('bot', 'width', 'file_id', 'file_size', 'height', 'file_unique_id', '_id_attrs')
+
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ width: int,
+ height: int,
+ file_size: int = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = str(file_id)
+ self.file_unique_id = str(file_unique_id)
self.width = int(width)
self.height = int(height)
# Optionals
self.file_size = file_size
self.bot = bot
- self._id_attrs = (self.file_id,)
-
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- return cls(bot=bot, **data)
-
- @classmethod
- def de_list(cls, data, bot):
- if not data:
- return []
-
- photos = list()
- for photo in data:
- photos.append(cls.de_json(photo, bot))
-
- return photos
+ self._id_attrs = (self.file_unique_id,)
- def get_file(self, timeout=None, **kwargs):
+ def get_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
- Args:
- timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout from the server (instead of the one specified during creation of
- the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
Returns:
:class:`telegram.File`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
+ return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs)
diff --git a/telegramer/include/telegram/files/sticker.py b/telegramer/include/telegram/files/sticker.py
index e42509d..e3f22a9 100644
--- a/telegramer/include/telegram/files/sticker.py
+++ b/telegramer/include/telegram/files/sticker.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,30 +18,40 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains objects that represents stickers."""
-from telegram import PhotoSize, TelegramObject
+from typing import TYPE_CHECKING, Any, List, Optional, ClassVar
+
+from telegram import PhotoSize, TelegramObject, constants
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File
class Sticker(TelegramObject):
"""This object represents a sticker.
- Attributes:
- file_id (:obj:`str`): Unique identifier for this file.
- width (:obj:`int`): Sticker width.
- height (:obj:`int`): Sticker height.
- thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg
- format.
- emoji (:obj:`str`): Optional. Emoji associated with the sticker.
- set_name (:obj:`str`): Optional. Name of the sticker set to which the sticker belongs.
- mask_position (:class:`telegram.MaskPosition`): Optional. For mask stickers, the position
- where the mask should be placed.
- file_size (:obj:`int`): Optional. File size.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
+
+ Note:
+ As of v13.11 ``is_video`` is a required argument and therefore the order of the
+ arguments had to be changed. Use keyword arguments to make sure that the arguments are
+ passed correctly.
Args:
- file_id (:obj:`str`): Unique identifier for this file.
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
width (:obj:`int`): Sticker width.
height (:obj:`int`): Sticker height.
- thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .webp or .jpg
+ is_animated (:obj:`bool`): :obj:`True`, if the sticker is animated.
+ is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker.
+
+ .. versionadded:: 13.11
+ thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .WEBP or .JPG
format.
emoji (:obj:`str`, optional): Emoji associated with the sticker
set_name (:obj:`str`, optional): Name of the sticker set to which the sticker
@@ -49,26 +59,70 @@ class Sticker(TelegramObject):
mask_position (:class:`telegram.MaskPosition`, optional): For mask stickers, the
position where the mask should be placed.
file_size (:obj:`int`, optional): File size.
- **kwargs (obj:`dict`): Arbitrary keyword arguments.7
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
+ **kwargs (obj:`dict`): Arbitrary keyword arguments.
+
+ Attributes:
+ file_id (:obj:`str`): Identifier for this file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ width (:obj:`int`): Sticker width.
+ height (:obj:`int`): Sticker height.
+ is_animated (:obj:`bool`): :obj:`True`, if the sticker is animated.
+ is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker.
+
+ .. versionadded:: 13.11
+ thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg
+ format.
+ emoji (:obj:`str`): Optional. Emoji associated with the sticker.
+ set_name (:obj:`str`): Optional. Name of the sticker set to which the sticker belongs.
+ mask_position (:class:`telegram.MaskPosition`): Optional. For mask stickers, the position
+ where the mask should be placed.
+ file_size (:obj:`int`): Optional. File size.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
"""
- def __init__(self,
- file_id,
- width,
- height,
- thumb=None,
- emoji=None,
- file_size=None,
- set_name=None,
- mask_position=None,
- bot=None,
- **kwargs):
+ __slots__ = (
+ 'bot',
+ 'width',
+ 'file_id',
+ 'is_animated',
+ 'is_video',
+ 'file_size',
+ 'thumb',
+ 'set_name',
+ 'mask_position',
+ 'height',
+ 'file_unique_id',
+ 'emoji',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ width: int,
+ height: int,
+ is_animated: bool,
+ is_video: bool,
+ thumb: PhotoSize = None,
+ emoji: str = None,
+ file_size: int = None,
+ set_name: str = None,
+ mask_position: 'MaskPosition' = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = str(file_id)
+ self.file_unique_id = str(file_unique_id)
self.width = int(width)
self.height = int(height)
+ self.is_animated = is_animated
+ self.is_video = is_video
# Optionals
self.thumb = thumb
self.emoji = emoji
@@ -77,84 +131,122 @@ def __init__(self,
self.mask_position = mask_position
self.bot = bot
- self._id_attrs = (self.file_id,)
+ self._id_attrs = (self.file_unique_id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Sticker']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(Sticker, cls).de_json(data, bot)
-
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
data['mask_position'] = MaskPosition.de_json(data.get('mask_position'), bot)
return cls(bot=bot, **data)
- @classmethod
- def de_list(cls, data, bot):
- if not data:
- return list()
-
- return [cls.de_json(d, bot) for d in data]
-
- def get_file(self, timeout=None, **kwargs):
+ def get_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
- Args:
- timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout from the server (instead of the one specified during creation of
- the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
Returns:
:class:`telegram.File`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
+ return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs)
class StickerSet(TelegramObject):
"""This object represents a sticker set.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`name` is equal.
+
+ Note:
+ As of v13.11 ``is_video`` is a required argument and therefore the order of the
+ arguments had to be changed. Use keyword arguments to make sure that the arguments are
+ passed correctly.
+
+ Args:
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
- contains_masks (:obj:`bool`): True, if the sticker set contains masks.
+ is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers.
+ is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
+
+ .. versionadded:: 13.11
+ contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
+ thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the ``.WEBP``,
+ ``.TGS``, or ``.WEBM`` format.
- Args:
+ Attributes:
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
- contains_masks (:obj:`bool`): True, if the sticker set contains masks.
+ is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers.
+ is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
+
+ .. versionadded:: 13.11
+ contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
+ thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the ``.WEBP``,
+ ``.TGS`` or ``.WEBM`` format.
"""
- def __init__(self, name, title, contains_masks, stickers, bot=None, **kwargs):
+ __slots__ = (
+ 'is_animated',
+ 'is_video',
+ 'contains_masks',
+ 'thumb',
+ 'title',
+ 'stickers',
+ 'name',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ name: str,
+ title: str,
+ is_animated: bool,
+ contains_masks: bool,
+ stickers: List[Sticker],
+ is_video: bool,
+ thumb: PhotoSize = None,
+ **_kwargs: Any,
+ ):
self.name = name
self.title = title
+ self.is_animated = is_animated
+ self.is_video = is_video
self.contains_masks = contains_masks
self.stickers = stickers
+ # Optionals
+ self.thumb = thumb
self._id_attrs = (self.name,)
- @staticmethod
- def de_json(data, bot):
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['StickerSet']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
if not data:
return None
- data = super(StickerSet, StickerSet).de_json(data, bot)
-
+ data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
data['stickers'] = Sticker.de_list(data.get('stickers'), bot)
- return StickerSet(bot=bot, **data)
+ return cls(bot=bot, **data)
- def to_dict(self):
- data = super(StickerSet, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
data['stickers'] = [s.to_dict() for s in data.get('stickers')]
@@ -164,20 +256,26 @@ def to_dict(self):
class MaskPosition(TelegramObject):
"""This object describes the position on faces where a mask should be placed by default.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`point`, :attr:`x_shift`, :attr:`y_shift` and, :attr:`scale`
+ are equal.
+
Attributes:
point (:obj:`str`): The part of the face relative to which the mask should be placed.
+ One of ``'forehead'``, ``'eyes'``, ``'mouth'``, or ``'chin'``.
x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face
size, from left to right.
y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face
size, from top to bottom.
scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size.
- Notes:
+ Note:
:attr:`type` should be one of the following: `forehead`, `eyes`, `mouth` or `chin`. You can
- use the classconstants for those.
+ use the class constants for those.
Args:
point (:obj:`str`): The part of the face relative to which the mask should be placed.
+ One of ``'forehead'``, ``'eyes'``, ``'mouth'``, or ``'chin'``.
x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face
size, from left to right. For example, choosing -1.0 will place mask just to the left
of the default mask position.
@@ -187,23 +285,31 @@ class MaskPosition(TelegramObject):
scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size.
"""
- FOREHEAD = 'forehead'
- """:obj:`str`: 'forehead'"""
- EYES = 'eyes'
- """:obj:`str`: 'eyes'"""
- MOUTH = 'mouth'
- """:obj:`str`: 'mouth'"""
- CHIN = 'chin'
- """:obj:`str`: 'chin'"""
-
- def __init__(self, point, x_shift, y_shift, scale, **kwargs):
+
+ __slots__ = ('point', 'scale', 'x_shift', 'y_shift', '_id_attrs')
+
+ FOREHEAD: ClassVar[str] = constants.STICKER_FOREHEAD
+ """:const:`telegram.constants.STICKER_FOREHEAD`"""
+ EYES: ClassVar[str] = constants.STICKER_EYES
+ """:const:`telegram.constants.STICKER_EYES`"""
+ MOUTH: ClassVar[str] = constants.STICKER_MOUTH
+ """:const:`telegram.constants.STICKER_MOUTH`"""
+ CHIN: ClassVar[str] = constants.STICKER_CHIN
+ """:const:`telegram.constants.STICKER_CHIN`"""
+
+ def __init__(self, point: str, x_shift: float, y_shift: float, scale: float, **_kwargs: Any):
self.point = point
self.x_shift = x_shift
self.y_shift = y_shift
self.scale = scale
+ self._id_attrs = (self.point, self.x_shift, self.y_shift, self.scale)
+
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['MaskPosition']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if data is None:
return None
diff --git a/telegramer/include/telegram/files/venue.py b/telegramer/include/telegram/files/venue.py
index 27779ea..aad46db 100644
--- a/telegramer/include/telegram/files/venue.py
+++ b/telegramer/include/telegram/files/venue.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,19 +18,24 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Venue."""
-from telegram import TelegramObject, Location
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import Location, TelegramObject
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class Venue(TelegramObject):
"""This object represents a venue.
- Attributes:
- location (:class:`telegram.Location`): Venue location.
- title (:obj:`str`): Name of the venue.
- address (:obj:`str`): Address of the venue.
- foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue.
- foursquare_type (:obj:`str`): Optional. Foursquare type of the venue. (For example,
- "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".)
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`location` and :attr:`title` are equal.
+
+ Note:
+ Foursquare details and Google Pace details are mutually exclusive. However, this
+ behaviour is undocumented and might be changed by Telegram.
Args:
location (:class:`telegram.Location`): Venue location.
@@ -39,12 +44,44 @@ class Venue(TelegramObject):
foursquare_id (:obj:`str`, optional): Foursquare identifier of the venue.
foursquare_type (:obj:`str`, optional): Foursquare type of the venue. (For example,
"arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".)
+ google_place_id (:obj:`str`, optional): Google Places identifier of the venue.
+ google_place_type (:obj:`str`, optional): Google Places type of the venue. (See
+ `supported types `_.)
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ location (:class:`telegram.Location`): Venue location.
+ title (:obj:`str`): Name of the venue.
+ address (:obj:`str`): Address of the venue.
+ foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue.
+ foursquare_type (:obj:`str`): Optional. Foursquare type of the venue.
+ google_place_id (:obj:`str`): Optional. Google Places identifier of the venue.
+ google_place_type (:obj:`str`): Optional. Google Places type of the venue.
+
"""
- def __init__(self, location, title, address, foursquare_id=None, foursquare_type=None,
- **kwargs):
+ __slots__ = (
+ 'google_place_type',
+ 'location',
+ 'title',
+ 'address',
+ 'foursquare_type',
+ 'foursquare_id',
+ 'google_place_id',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ location: Location,
+ title: str,
+ address: str,
+ foursquare_id: str = None,
+ foursquare_type: str = None,
+ google_place_id: str = None,
+ google_place_type: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.location = location
self.title = title
@@ -52,12 +89,15 @@ def __init__(self, location, title, address, foursquare_id=None, foursquare_type
# Optionals
self.foursquare_id = foursquare_id
self.foursquare_type = foursquare_type
+ self.google_place_id = google_place_id
+ self.google_place_type = google_place_type
self._id_attrs = (self.location, self.title)
@classmethod
- def de_json(cls, data, bot):
- data = super(Venue, cls).de_json(data, bot)
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Venue']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
if not data:
return None
diff --git a/telegramer/include/telegram/files/video.py b/telegramer/include/telegram/files/video.py
index 8acc4bb..92e7c5e 100644
--- a/telegramer/include/telegram/files/video.py
+++ b/telegramer/include/telegram/files/video.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,83 +18,121 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Video."""
+from typing import TYPE_CHECKING, Any, Optional
+
from telegram import PhotoSize, TelegramObject
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File
class Video(TelegramObject):
"""This object represents a video file.
- Attributes:
- file_id (:obj:`str`): Unique identifier for this file.
- width (:obj:`int`): Video width as defined by sender.
- height (:obj:`int`): Video height as defined by sender.
- duration (:obj:`int`): Duration of the video in seconds as defined by sender.
- thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
- mime_type (:obj:`str`): Optional. Mime type of a file as defined by sender.
- file_size (:obj:`int`): Optional. File size.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
Args:
- file_id (:obj:`str`): Unique identifier for this file.
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
width (:obj:`int`): Video width as defined by sender.
height (:obj:`int`): Video height as defined by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
thumb (:class:`telegram.PhotoSize`, optional): Video thumbnail.
+ file_name (:obj:`str`, optional): Original filename as defined by sender.
mime_type (:obj:`str`, optional): Mime type of a file as defined by sender.
file_size (:obj:`int`, optional): File size.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ file_id (:obj:`str`): Identifier for this file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ width (:obj:`int`): Video width as defined by sender.
+ height (:obj:`int`): Video height as defined by sender.
+ duration (:obj:`int`): Duration of the video in seconds as defined by sender.
+ thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
+ file_name (:obj:`str`): Optional. Original filename as defined by sender.
+ mime_type (:obj:`str`): Optional. Mime type of a file as defined by sender.
+ file_size (:obj:`int`): Optional. File size.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+
"""
- def __init__(self,
- file_id,
- width,
- height,
- duration,
- thumb=None,
- mime_type=None,
- file_size=None,
- bot=None,
- **kwargs):
+ __slots__ = (
+ 'bot',
+ 'width',
+ 'file_id',
+ 'file_size',
+ 'file_name',
+ 'thumb',
+ 'duration',
+ 'mime_type',
+ 'height',
+ 'file_unique_id',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ width: int,
+ height: int,
+ duration: int,
+ thumb: PhotoSize = None,
+ mime_type: str = None,
+ file_size: int = None,
+ bot: 'Bot' = None,
+ file_name: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = str(file_id)
+ self.file_unique_id = str(file_unique_id)
self.width = int(width)
self.height = int(height)
self.duration = int(duration)
# Optionals
self.thumb = thumb
+ self.file_name = file_name
self.mime_type = mime_type
self.file_size = file_size
self.bot = bot
- self._id_attrs = (self.file_id,)
+ self._id_attrs = (self.file_unique_id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Video']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(Video, cls).de_json(data, bot)
-
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
return cls(bot=bot, **data)
- def get_file(self, timeout=None, **kwargs):
+ def get_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
- Args:
- timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout from the server (instead of the one specified during creation of
- the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
Returns:
:class:`telegram.File`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
+ return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs)
diff --git a/telegramer/include/telegram/files/videonote.py b/telegramer/include/telegram/files/videonote.py
index 5201a58..17d6207 100644
--- a/telegramer/include/telegram/files/videonote.py
+++ b/telegramer/include/telegram/files/videonote.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,34 +18,74 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram VideoNote."""
+from typing import TYPE_CHECKING, Optional, Any
+
from telegram import PhotoSize, TelegramObject
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File
class VideoNote(TelegramObject):
"""This object represents a video message (available in Telegram apps as of v.4.0).
- Attributes:
- file_id (:obj:`str`): Unique identifier for this file.
- length (:obj:`int`): Video width and height as defined by sender.
- duration (:obj:`int`): Duration of the video in seconds as defined by sender.
- thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
- file_size (:obj:`int`): Optional. File size.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
Args:
- file_id (:obj:`str`): Unique identifier for this file.
- length (:obj:`int`): Video width and height as defined by sender.
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ length (:obj:`int`): Video width and height (diameter of the video message) as defined
+ by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
thumb (:class:`telegram.PhotoSize`, optional): Video thumbnail.
file_size (:obj:`int`, optional): File size.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ file_id (:obj:`str`): Identifier for this file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ length (:obj:`int`): Video width and height as defined by sender.
+ duration (:obj:`int`): Duration of the video in seconds as defined by sender.
+ thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
+ file_size (:obj:`int`): Optional. File size.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+
"""
- def __init__(self, file_id, length, duration, thumb=None, file_size=None, bot=None, **kwargs):
+ __slots__ = (
+ 'bot',
+ 'length',
+ 'file_id',
+ 'file_size',
+ 'thumb',
+ 'duration',
+ 'file_unique_id',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ length: int,
+ duration: int,
+ thumb: PhotoSize = None,
+ file_size: int = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = str(file_id)
+ self.file_unique_id = str(file_unique_id)
self.length = int(length)
self.duration = int(duration)
# Optionals
@@ -53,33 +93,32 @@ def __init__(self, file_id, length, duration, thumb=None, file_size=None, bot=No
self.file_size = file_size
self.bot = bot
- self._id_attrs = (self.file_id,)
+ self._id_attrs = (self.file_unique_id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['VideoNote']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(VideoNote, cls).de_json(data, bot)
-
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
return cls(bot=bot, **data)
- def get_file(self, timeout=None, **kwargs):
+ def get_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
- Args:
- timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout from the server (instead of the one specified during creation of
- the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
Returns:
:class:`telegram.File`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
+ return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs)
diff --git a/telegramer/include/telegram/files/voice.py b/telegramer/include/telegram/files/voice.py
index 41d5bc7..c878282 100644
--- a/telegramer/include/telegram/files/voice.py
+++ b/telegramer/include/telegram/files/voice.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,63 +18,89 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Voice."""
+from typing import TYPE_CHECKING, Any
+
from telegram import TelegramObject
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File
class Voice(TelegramObject):
"""This object represents a voice note.
- Attributes:
- file_id (:obj:`str`): Unique identifier for this file.
- duration (:obj:`int`): Duration of the audio in seconds as defined by sender.
- mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
- file_size (:obj:`int`): Optional. File size.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
Args:
- file_id (:obj:`str`): Unique identifier for this file.
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
duration (:obj:`int`, optional): Duration of the audio in seconds as defined by sender.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
file_size (:obj:`int`, optional): File size.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ file_id (:obj:`str`): Identifier for this file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ duration (:obj:`int`): Duration of the audio in seconds as defined by sender.
+ mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
+ file_size (:obj:`int`): Optional. File size.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+
"""
- def __init__(self, file_id, duration, mime_type=None, file_size=None, bot=None, **kwargs):
+ __slots__ = (
+ 'bot',
+ 'file_id',
+ 'file_size',
+ 'duration',
+ 'mime_type',
+ 'file_unique_id',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ duration: int,
+ mime_type: str = None,
+ file_size: int = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = str(file_id)
+ self.file_unique_id = str(file_unique_id)
self.duration = int(duration)
# Optionals
self.mime_type = mime_type
self.file_size = file_size
self.bot = bot
- self._id_attrs = (self.file_id,)
-
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- data = super(Voice, cls).de_json(data, bot)
-
- return cls(bot=bot, **data)
+ self._id_attrs = (self.file_unique_id,)
- def get_file(self, timeout=None, **kwargs):
+ def get_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
- Args:
- timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout from the server (instead of the one specified during creation of
- the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
Returns:
:class:`telegram.File`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
+ return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs)
diff --git a/telegramer/include/telegram/forcereply.py b/telegramer/include/telegram/forcereply.py
index fa89f31..792b211 100644
--- a/telegramer/include/telegram/forcereply.py
+++ b/telegramer/include/telegram/forcereply.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,6 +18,8 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ForceReply."""
+from typing import Any
+
from telegram import ReplyMarkup
@@ -28,24 +30,49 @@ class ForceReply(ReplyMarkup):
extremely useful if you want to create user-friendly step-by-step interfaces without having
to sacrifice privacy mode.
- Attributes:
- force_reply (:obj:`True`): Shows reply interface to the user.
- selective (:obj:`bool`): Optional. Force reply from specific users only.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`selective` is equal.
Args:
selective (:obj:`bool`, optional): Use this parameter if you want to force reply from
specific users only. Targets:
- 1) users that are @mentioned in the text of the Message object
- 2) if the bot's message is a reply (has reply_to_message_id), sender of the
+ 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the
+ :class:`telegram.Message` object.
+ 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
original message.
+ input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input
+ field when the reply is active; 1-64 characters.
+
+ .. versionadded:: 13.7
+
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ force_reply (:obj:`True`): Shows reply interface to the user, as if they manually selected
+ the bots message and tapped 'Reply'.
+ selective (:obj:`bool`): Optional. Force reply from specific users only.
+ input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
+ field when the reply is active.
+
+ .. versionadded:: 13.7
+
"""
- def __init__(self, force_reply=True, selective=False, **kwargs):
+ __slots__ = ('selective', 'force_reply', 'input_field_placeholder', '_id_attrs')
+
+ def __init__(
+ self,
+ force_reply: bool = True,
+ selective: bool = False,
+ input_field_placeholder: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.force_reply = bool(force_reply)
# Optionals
self.selective = bool(selective)
+ self.input_field_placeholder = input_field_placeholder
+
+ self._id_attrs = (self.selective,)
diff --git a/telegramer/include/telegram/games/animation.py b/telegramer/include/telegram/games/animation.py
deleted file mode 100644
index f8f02c6..0000000
--- a/telegramer/include/telegram/games/animation.py
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/usr/bin/env python
-#
-# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2017
-# Leandro Toledo de Souza
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser Public License for more details.
-#
-# You should have received a copy of the GNU Lesser Public License
-# along with this program. If not, see [http://www.gnu.org/licenses/].
-"""This module contains an object that represents a Telegram Animation."""
-from telegram import PhotoSize
-from telegram import TelegramObject
-
-
-class Animation(TelegramObject):
- """This object represents an animation file to be displayed in the message containing a game.
-
- Attributes:
- file_id (:obj:`str`): Unique file identifier.
- thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined
- by sender.
- file_name (:obj:`str`): Optional. Original animation filename as defined by sender.
- mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
- file_size (:obj:`int`): Optional. File size.
-
- Args:
- file_id (:obj:`str`): Unique file identifier.
- thumb (:class:`telegram.PhotoSize`, optional): Animation thumbnail as defined by sender.
- file_name (:obj:`str`, optional): Original animation filename as defined by sender.
- mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
- file_size (:obj:`int`, optional): File size.
-
- """
-
- def __init__(self,
- file_id,
- thumb=None,
- file_name=None,
- mime_type=None,
- file_size=None,
- **kwargs):
- self.file_id = file_id
- self.thumb = thumb
- self.file_name = file_name
- self.mime_type = mime_type
- self.file_size = file_size
-
- self._id_attrs = (self.file_id,)
-
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- data = super(Animation, cls).de_json(data, bot)
-
- data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
-
- return cls(**data)
diff --git a/telegramer/include/telegram/games/callbackgame.py b/telegramer/include/telegram/games/callbackgame.py
index 0b88d72..6803a44 100644
--- a/telegramer/include/telegram/games/callbackgame.py
+++ b/telegramer/include/telegram/games/callbackgame.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -23,3 +23,5 @@
class CallbackGame(TelegramObject):
"""A placeholder, currently holds no information. Use BotFather to set up your game."""
+
+ __slots__ = ()
diff --git a/telegramer/include/telegram/games/game.py b/telegramer/include/telegram/games/game.py
index 089ac8d..86eb2ab 100644
--- a/telegramer/include/telegram/games/game.py
+++ b/telegramer/include/telegram/games/game.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,74 +19,102 @@
"""This module contains an object that represents a Telegram Game."""
import sys
+from typing import TYPE_CHECKING, Any, Dict, List, Optional
-from telegram import MessageEntity, TelegramObject, Animation, PhotoSize
+from telegram import Animation, MessageEntity, PhotoSize, TelegramObject
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class Game(TelegramObject):
"""
- This object represents a game. Use BotFather to create and edit games, their short names will
- act as unique identifiers.
+ This object represents a game. Use `BotFather `_ to create and edit
+ games, their short names will act as unique identifiers.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`title`, :attr:`description` and :attr:`photo` are equal.
+
+ Args:
title (:obj:`str`): Title of the game.
description (:obj:`str`): Description of the game.
photo (List[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message
in chats.
- text (:obj:`str`): Optional. Brief description of the game or high scores included in the
+ text (:obj:`str`, optional): Brief description of the game or high scores included in the
game message. Can be automatically edited to include current high scores for the game
- when the bot calls set_game_score, or manually edited using edit_message_text.
- text_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities that
+ when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited
+ using :meth:`telegram.Bot.edit_message_text`.
+ 0-4096 characters. Also found as ``telegram.constants.MAX_MESSAGE_LENGTH``.
+ text_entities (List[:class:`telegram.MessageEntity`], optional): Special entities that
appear in text, such as usernames, URLs, bot commands, etc.
- animation (:class:`telegram.Animation`): Optional. Animation that will be displayed in the
- game message in chats. Upload via BotFather.
+ animation (:class:`telegram.Animation`, optional): Animation that will be displayed in the
+ game message in chats. Upload via `BotFather `_.
- Args:
+ Attributes:
title (:obj:`str`): Title of the game.
description (:obj:`str`): Description of the game.
photo (List[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message
in chats.
- text (:obj:`str`, optional): Brief description of the game or high scores included in the
+ text (:obj:`str`): Optional. Brief description of the game or high scores included in the
game message. Can be automatically edited to include current high scores for the game
- when the bot calls set_game_score, or manually edited using edit_message_text.
- 0-4096 characters. Also found as ``telegram.constants.MAX_MESSAGE_LENGTH``.
- text_entities (List[:class:`telegram.MessageEntity`], optional): Special entities that
+ when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited
+ using :meth:`telegram.Bot.edit_message_text`.
+ text_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities that
appear in text, such as usernames, URLs, bot commands, etc.
- animation (:class:`telegram.Animation`, optional): Animation that will be displayed in the
- game message in chats. Upload via BotFather.
+ animation (:class:`telegram.Animation`): Optional. Animation that will be displayed in the
+ game message in chats. Upload via `BotFather `_.
"""
- def __init__(self,
- title,
- description,
- photo,
- text=None,
- text_entities=None,
- animation=None,
- **kwargs):
+ __slots__ = (
+ 'title',
+ 'photo',
+ 'description',
+ 'text_entities',
+ 'text',
+ 'animation',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ title: str,
+ description: str,
+ photo: List[PhotoSize],
+ text: str = None,
+ text_entities: List[MessageEntity] = None,
+ animation: Animation = None,
+ **_kwargs: Any,
+ ):
+ # Required
self.title = title
self.description = description
self.photo = photo
+ # Optionals
self.text = text
- self.text_entities = text_entities or list()
+ self.text_entities = text_entities or []
self.animation = animation
+ self._id_attrs = (self.title, self.description, self.photo)
+
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Game']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(Game, cls).de_json(data, bot)
-
data['photo'] = PhotoSize.de_list(data.get('photo'), bot)
data['text_entities'] = MessageEntity.de_list(data.get('text_entities'), bot)
data['animation'] = Animation.de_json(data.get('animation'), bot)
return cls(**data)
- def to_dict(self):
- data = super(Game, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
data['photo'] = [p.to_dict() for p in self.photo]
if self.text_entities:
@@ -94,7 +122,7 @@ def to_dict(self):
return data
- def parse_text_entity(self, entity):
+ def parse_text_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
Note:
@@ -109,17 +137,22 @@ def parse_text_entity(self, entity):
Returns:
:obj:`str`: The text of the given entity.
+ Raises:
+ RuntimeError: If this game has no text.
+
"""
+ if not self.text:
+ raise RuntimeError("This Game has no 'text'.")
+
# Is it a narrow build, if so we don't need to convert
- if sys.maxunicode == 0xffff:
- return self.text[entity.offset:entity.offset + entity.length]
- else:
- entity_text = self.text.encode('utf-16-le')
- entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2]
+ if sys.maxunicode == 0xFFFF:
+ return self.text[entity.offset : entity.offset + entity.length]
+ entity_text = self.text.encode('utf-16-le')
+ entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode('utf-16-le')
- def parse_text_entities(self, types=None):
+ def parse_text_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this message filtered by their ``type`` attribute as the key, and
@@ -145,5 +178,9 @@ def parse_text_entities(self, types=None):
return {
entity: self.parse_text_entity(entity)
- for entity in self.text_entities if entity.type in types
+ for entity in (self.text_entities or [])
+ if entity.type in types
}
+
+ def __hash__(self) -> int:
+ return hash((self.title, self.description, tuple(p for p in self.photo)))
diff --git a/telegramer/include/telegram/games/gamehighscore.py b/telegramer/include/telegram/games/gamehighscore.py
index aea2939..5967dda 100644
--- a/telegramer/include/telegram/games/gamehighscore.py
+++ b/telegramer/include/telegram/games/gamehighscore.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,36 +18,50 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram GameHighScore."""
+from typing import TYPE_CHECKING, Optional
+
from telegram import TelegramObject, User
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class GameHighScore(TelegramObject):
"""This object represents one row of the high scores table for a game.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`position`, :attr:`user` and :attr:`score` are equal.
+
+ Args:
position (:obj:`int`): Position in high score table for the game.
user (:class:`telegram.User`): User.
score (:obj:`int`): Score.
- Args:
+ Attributes:
position (:obj:`int`): Position in high score table for the game.
user (:class:`telegram.User`): User.
score (:obj:`int`): Score.
"""
- def __init__(self, position, user, score):
+ __slots__ = ('position', 'user', 'score', '_id_attrs')
+
+ def __init__(self, position: int, user: User, score: int):
self.position = position
self.user = user
self.score = score
+ self._id_attrs = (self.position, self.user, self.score)
+
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['GameHighScore']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(GameHighScore, cls).de_json(data, bot)
-
data['user'] = User.de_json(data.get('user'), bot)
return cls(**data)
diff --git a/telegramer/include/telegram/inline/inlinekeyboardbutton.py b/telegramer/include/telegram/inline/inlinekeyboardbutton.py
index d949cf9..49b6e07 100644
--- a/telegramer/include/telegram/inline/inlinekeyboardbutton.py
+++ b/telegramer/include/telegram/inline/inlinekeyboardbutton.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,35 +18,57 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InlineKeyboardButton."""
+from typing import TYPE_CHECKING, Any
+
from telegram import TelegramObject
+if TYPE_CHECKING:
+ from telegram import CallbackGame, LoginUrl
+
class InlineKeyboardButton(TelegramObject):
"""This object represents one button of an inline keyboard.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`text`, :attr:`url`, :attr:`login_url`, :attr:`callback_data`,
+ :attr:`switch_inline_query`, :attr:`switch_inline_query_current_chat`, :attr:`callback_game`
+ and :attr:`pay` are equal.
+
Note:
- You must use exactly one of the optional fields. Mind that :attr:`callback_game` is not
- working as expected. Putting a game short name in it might, but is not guaranteed to work.
+ * You must use exactly one of the optional fields. Mind that :attr:`callback_game` is not
+ working as expected. Putting a game short name in it might, but is not guaranteed to
+ work.
+ * If your bot allows for arbitrary callback data, in keyboards returned in a response
+ from telegram, :attr:`callback_data` maybe be an instance of
+ :class:`telegram.ext.InvalidCallbackData`. This will be the case, if the data
+ associated with the button was already deleted.
- Attributes:
- text (:obj:`str`): Label text on the button.
- url (:obj:`str`): Optional. HTTP url to be opened when button is pressed.
- callback_data (:obj:`str`): Optional. Data to be sent in a callback query to the bot when
- button is pressed, 1-64 bytes.
- switch_inline_query (:obj:`str`): Optional. Will prompt the user to select one of their
- chats, open that chat and insert the bot's username and the specified inline query in
- the input field.
- switch_inline_query_current_chat (:obj:`str`): Optional. Will insert the bot's username and
- the specified inline query in the current chat's input field.
- callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will
- be launched when the user presses the button.
- pay (:obj:`bool`): Optional. Specify True, to send a Pay button.
+ .. versionadded:: 13.6
+
+ * Since Bot API 5.5, it's now allowed to mention users by their ID in inline keyboards.
+ This will only work in Telegram versions released after December 7, 2021.
+ Older clients will display *unsupported message*.
+
+ Warning:
+ If your bot allows your arbitrary callback data, buttons whose callback data is a
+ non-hashable object will become unhashable. Trying to evaluate ``hash(button)`` will
+ result in a :class:`TypeError`.
+
+ .. versionchanged:: 13.6
Args:
text (:obj:`str`): Label text on the button.
- url (:obj:`str`): HTTP url to be opened when button is pressed.
- callback_data (:obj:`str`, optional): Data to be sent in a callback query to the bot when
- button is pressed, 1-64 bytes.
+ url (:obj:`str`, optional): HTTP or tg:// url to be opened when the button is pressed.
+ Links ``tg://user?id=`` can be used to mention a user by
+ their ID without using a username, if this is allowed by their privacy settings.
+
+ .. versionchanged:: 13.9
+ You can now mention a user using ``tg://user?id=``.
+ login_url (:class:`telegram.LoginUrl`, optional): An HTTP URL used to automatically
+ authorize the user. Can be used as a replacement for the Telegram Login Widget.
+ callback_data (:obj:`str` | :obj:`Any`, optional): Data to be sent in a callback query to
+ the bot when button is pressed, UTF-8 1-64 bytes. If the bot instance allows arbitrary
+ callback data, anything can be passed.
switch_inline_query (:obj:`str`, optional): If set, pressing the button will prompt the
user to select one of their chats, open that chat and insert the bot's username and the
specified inline query in the input field. Can be empty, in which case just the bot's
@@ -62,28 +84,94 @@ class InlineKeyboardButton(TelegramObject):
callback_game (:class:`telegram.CallbackGame`, optional): Description of the game that will
be launched when the user presses the button. This type of button must always be
the ``first`` button in the first row.
- pay (:obj:`bool`, optional): Specify True, to send a Pay button. This type of button must
- always be the ``first`` button in the first row.
+ pay (:obj:`bool`, optional): Specify :obj:`True`, to send a Pay button. This type of button
+ must always be the `first` button in the first row and can only be used in invoice
+ messages.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ text (:obj:`str`): Label text on the button.
+ url (:obj:`str`): Optional. HTTP or tg:// url to be opened when the button is pressed.
+ Links ``tg://user?id=`` can be used to mention a user by
+ their ID without using a username, if this is allowed by their privacy settings.
+
+ .. versionchanged:: 13.9
+ You can now mention a user using ``tg://user?id=``.
+ login_url (:class:`telegram.LoginUrl`): Optional. An HTTP URL used to automatically
+ authorize the user. Can be used as a replacement for the Telegram Login Widget.
+ callback_data (:obj:`str` | :obj:`object`): Optional. Data to be sent in a callback query
+ to the bot when button is pressed, UTF-8 1-64 bytes.
+ switch_inline_query (:obj:`str`): Optional. Will prompt the user to select one of their
+ chats, open that chat and insert the bot's username and the specified inline query in
+ the input field. Can be empty, in which case just the bot’s username will be inserted.
+ switch_inline_query_current_chat (:obj:`str`): Optional. Will insert the bot's username and
+ the specified inline query in the current chat's input field. Can be empty, in which
+ case just the bot’s username will be inserted.
+ callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will
+ be launched when the user presses the button.
+ pay (:obj:`bool`): Optional. Specify :obj:`True`, to send a Pay button.
+
"""
- def __init__(self,
- text,
- url=None,
- callback_data=None,
- switch_inline_query=None,
- switch_inline_query_current_chat=None,
- callback_game=None,
- pay=None,
- **kwargs):
+ __slots__ = (
+ 'callback_game',
+ 'url',
+ 'switch_inline_query_current_chat',
+ 'callback_data',
+ 'pay',
+ 'switch_inline_query',
+ 'text',
+ '_id_attrs',
+ 'login_url',
+ )
+
+ def __init__(
+ self,
+ text: str,
+ url: str = None,
+ callback_data: object = None,
+ switch_inline_query: str = None,
+ switch_inline_query_current_chat: str = None,
+ callback_game: 'CallbackGame' = None,
+ pay: bool = None,
+ login_url: 'LoginUrl' = None,
+ **_kwargs: Any,
+ ):
# Required
self.text = text
# Optionals
self.url = url
+ self.login_url = login_url
self.callback_data = callback_data
self.switch_inline_query = switch_inline_query
self.switch_inline_query_current_chat = switch_inline_query_current_chat
self.callback_game = callback_game
self.pay = pay
+ self._id_attrs = ()
+ self._set_id_attrs()
+
+ def _set_id_attrs(self) -> None:
+ self._id_attrs = (
+ self.text,
+ self.url,
+ self.login_url,
+ self.callback_data,
+ self.switch_inline_query,
+ self.switch_inline_query_current_chat,
+ self.callback_game,
+ self.pay,
+ )
+
+ def update_callback_data(self, callback_data: object) -> None:
+ """
+ Sets :attr:`callback_data` to the passed object. Intended to be used by
+ :class:`telegram.ext.CallbackDataCache`.
+
+ .. versionadded:: 13.6
+
+ Args:
+ callback_data (:obj:`obj`): The new callback data.
+ """
+ self.callback_data = callback_data
+ self._set_id_attrs()
diff --git a/telegramer/include/telegram/inline/inlinekeyboardmarkup.py b/telegramer/include/telegram/inline/inlinekeyboardmarkup.py
index 26f77af..61724f0 100644
--- a/telegramer/include/telegram/inline/inlinekeyboardmarkup.py
+++ b/telegramer/include/telegram/inline/inlinekeyboardmarkup.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,33 +18,121 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InlineKeyboardMarkup."""
-from telegram import ReplyMarkup
+from typing import TYPE_CHECKING, Any, List, Optional
+
+from telegram import InlineKeyboardButton, ReplyMarkup
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class InlineKeyboardMarkup(ReplyMarkup):
"""
This object represents an inline keyboard that appears right next to the message it belongs to.
- Attributes:
- inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): Array of button rows,
- each represented by an Array of InlineKeyboardButton objects.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their the size of :attr:`inline_keyboard` and all the buttons are equal.
Args:
- inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): Array of button rows,
- each represented by an Array of InlineKeyboardButton objects.
+ inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows,
+ each represented by a list of InlineKeyboardButton objects.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows,
+ each represented by a list of InlineKeyboardButton objects.
+
"""
- def __init__(self, inline_keyboard, **kwargs):
+ __slots__ = ('inline_keyboard', '_id_attrs')
+
+ def __init__(self, inline_keyboard: List[List[InlineKeyboardButton]], **_kwargs: Any):
# Required
self.inline_keyboard = inline_keyboard
- def to_dict(self):
- data = super(InlineKeyboardMarkup, self).to_dict()
+ self._id_attrs = (self.inline_keyboard,)
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
data['inline_keyboard'] = []
for inline_keyboard in self.inline_keyboard:
data['inline_keyboard'].append([x.to_dict() for x in inline_keyboard])
return data
+
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['InlineKeyboardMarkup']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ keyboard = []
+ for row in data['inline_keyboard']:
+ tmp = []
+ for col in row:
+ btn = InlineKeyboardButton.de_json(col, bot)
+ if btn:
+ tmp.append(btn)
+ keyboard.append(tmp)
+
+ return cls(keyboard)
+
+ @classmethod
+ def from_button(cls, button: InlineKeyboardButton, **kwargs: object) -> 'InlineKeyboardMarkup':
+ """Shortcut for::
+
+ InlineKeyboardMarkup([[button]], **kwargs)
+
+ Return an InlineKeyboardMarkup from a single InlineKeyboardButton
+
+ Args:
+ button (:class:`telegram.InlineKeyboardButton`): The button to use in the markup
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
+ """
+ return cls([[button]], **kwargs)
+
+ @classmethod
+ def from_row(
+ cls, button_row: List[InlineKeyboardButton], **kwargs: object
+ ) -> 'InlineKeyboardMarkup':
+ """Shortcut for::
+
+ InlineKeyboardMarkup([button_row], **kwargs)
+
+ Return an InlineKeyboardMarkup from a single row of InlineKeyboardButtons
+
+ Args:
+ button_row (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the
+ markup
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
+ """
+ return cls([button_row], **kwargs)
+
+ @classmethod
+ def from_column(
+ cls, button_column: List[InlineKeyboardButton], **kwargs: object
+ ) -> 'InlineKeyboardMarkup':
+ """Shortcut for::
+
+ InlineKeyboardMarkup([[button] for button in button_column], **kwargs)
+
+ Return an InlineKeyboardMarkup from a single column of InlineKeyboardButtons
+
+ Args:
+ button_column (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the
+ markup
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
+ """
+ button_grid = [[button] for button in button_column]
+ return cls(button_grid, **kwargs)
+
+ def __hash__(self) -> int:
+ return hash(tuple(tuple(button for button in row) for row in self.inline_keyboard))
diff --git a/telegramer/include/telegram/inline/inlinequery.py b/telegramer/include/telegram/inline/inlinequery.py
index 79b7e12..1230151 100644
--- a/telegramer/include/telegram/inline/inlinequery.py
+++ b/telegramer/include/telegram/inline/inlinequery.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python
-# pylint: disable=R0902,R0912,R0913
+# pylint: disable=R0902,R0913
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,7 +19,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InlineQuery."""
-from telegram import TelegramObject, User, Location
+from typing import TYPE_CHECKING, Any, Optional, Union, Callable, ClassVar, Sequence
+
+from telegram import Location, TelegramObject, User, constants
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, InlineQueryResult
class InlineQuery(TelegramObject):
@@ -27,45 +34,73 @@ class InlineQuery(TelegramObject):
This object represents an incoming inline query. When the user sends an empty query, your bot
could return some default or trending results.
- Note:
- * In Python `from` is a reserved word, use `from_user` instead.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
- Attributes:
- id (:obj:`str`): Unique identifier for this query.
- from_user (:class:`telegram.User`): Sender.
- location (:class:`telegram.Location`): Optional. Sender location, only for bots that
- request user location.
- query (:obj:`str`): Text of the query (up to 512 characters).
- offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot.
+ Note:
+ In Python ``from`` is a reserved word, use ``from_user`` instead.
Args:
id (:obj:`str`): Unique identifier for this query.
from_user (:class:`telegram.User`): Sender.
+ query (:obj:`str`): Text of the query (up to 256 characters).
+ offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot.
+ chat_type (:obj:`str`, optional): Type of the chat, from which the inline query was sent.
+ Can be either :attr:`telegram.Chat.SENDER` for a private chat with the inline query
+ sender, :attr:`telegram.Chat.PRIVATE`, :attr:`telegram.Chat.GROUP`,
+ :attr:`telegram.Chat.SUPERGROUP` or :attr:`telegram.Chat.CHANNEL`. The chat type should
+ be always known for requests sent from official clients and most third-party clients,
+ unless the request was sent from a secret chat.
+
+ .. versionadded:: 13.5
location (:class:`telegram.Location`, optional): Sender location, only for bots that
request user location.
- query (:obj:`str`): Text of the query (up to 512 characters).
- offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ id (:obj:`str`): Unique identifier for this query.
+ from_user (:class:`telegram.User`): Sender.
+ query (:obj:`str`): Text of the query (up to 256 characters).
+ offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot.
+ location (:class:`telegram.Location`): Optional. Sender location, only for bots that
+ request user location.
+ chat_type (:obj:`str`, optional): Type of the chat, from which the inline query was sent.
+
+ .. versionadded:: 13.5
+
"""
- def __init__(self, id, from_user, query, offset, location=None, bot=None, **kwargs):
+ __slots__ = ('bot', 'location', 'chat_type', 'id', 'offset', 'from_user', 'query', '_id_attrs')
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ from_user: User,
+ query: str,
+ offset: str,
+ location: Location = None,
+ bot: 'Bot' = None,
+ chat_type: str = None,
+ **_kwargs: Any,
+ ):
# Required
- self.id = id
+ self.id = id # pylint: disable=C0103
self.from_user = from_user
self.query = query
self.offset = offset
# Optional
self.location = location
+ self.chat_type = chat_type
self.bot = bot
self._id_attrs = (self.id,)
@classmethod
- def de_json(cls, data, bot):
- data = super(InlineQuery, cls).de_json(data, bot)
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['InlineQuery']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
if not data:
return None
@@ -75,29 +110,59 @@ def de_json(cls, data, bot):
return cls(bot=bot, **data)
- def answer(self, *args, **kwargs):
+ def answer(
+ self,
+ results: Union[
+ Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]]
+ ],
+ cache_time: int = 300,
+ is_personal: bool = None,
+ next_offset: str = None,
+ switch_pm_text: str = None,
+ switch_pm_parameter: str = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ current_offset: str = None,
+ api_kwargs: JSONDict = None,
+ auto_pagination: bool = False,
+ ) -> bool:
"""Shortcut for::
- bot.answer_inline_query(update.inline_query.id, *args, **kwargs)
+ bot.answer_inline_query(update.inline_query.id,
+ *args,
+ current_offset=self.offset if auto_pagination else None,
+ **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.answer_inline_query`.
Args:
- results (List[:class:`telegram.InlineQueryResult`]): A list of results for the inline
- query.
- cache_time (:obj:`int`, optional): The maximum amount of time in seconds that the
- result of the inline query may be cached on the server. Defaults to 300.
- is_personal (:obj:`bool`, optional): Pass True, if results may be cached on the server
- side only for the user that sent the query. By default, results may be returned to
- any user who sends the same query.
- next_offset (:obj:`str`, optional): Pass the offset that a client should send in the
- next query with the same text to receive more results. Pass an empty string if
- there are no more results or if you don't support pagination. Offset length can't
- exceed 64 bytes.
- switch_pm_text (:obj:`str`, optional): If passed, clients will display a button with
- specified text that switches the user to a private chat with the bot and sends the
- bot a start message with the parameter switch_pm_parameter.
- switch_pm_parameter (:obj:`str`, optional): Deep-linking parameter for the /start
- message sent to the bot when user presses the switch button. 1-64 characters,
- only A-Z, a-z, 0-9, _ and - are allowed.
+ auto_pagination (:obj:`bool`, optional): If set to :obj:`True`, :attr:`offset` will be
+ passed as :attr:`current_offset` to :meth:`telegram.Bot.answer_inline_query`.
+ Defaults to :obj:`False`.
+ Raises:
+ TypeError: If both :attr:`current_offset` and :attr:`auto_pagination` are supplied.
"""
- return self.bot.answer_inline_query(self.id, *args, **kwargs)
+ if current_offset and auto_pagination:
+ # We raise TypeError instead of ValueError for backwards compatibility with versions
+ # which didn't check this here but let Python do the checking
+ raise TypeError('current_offset and auto_pagination are mutually exclusive!')
+ return self.bot.answer_inline_query(
+ inline_query_id=self.id,
+ current_offset=self.offset if auto_pagination else current_offset,
+ results=results,
+ cache_time=cache_time,
+ is_personal=is_personal,
+ next_offset=next_offset,
+ switch_pm_text=switch_pm_text,
+ switch_pm_parameter=switch_pm_parameter,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ MAX_RESULTS: ClassVar[int] = constants.MAX_INLINE_QUERY_RESULTS
+ """
+ :const:`telegram.constants.MAX_INLINE_QUERY_RESULTS`
+
+ .. versionadded:: 13.2
+ """
diff --git a/telegramer/include/telegram/inline/inlinequeryresult.py b/telegramer/include/telegram/inline/inlinequeryresult.py
index 6feaf56..f3227d4 100644
--- a/telegramer/include/telegram/inline/inlinequeryresult.py
+++ b/telegramer/include/telegram/inline/inlinequeryresult.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,28 +16,56 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=W0622
"""This module contains the classes that represent Telegram InlineQueryResult."""
+from typing import Any
+
from telegram import TelegramObject
+from telegram.utils.types import JSONDict
class InlineQueryResult(TelegramObject):
"""Baseclass for the InlineQueryResult* classes.
- Attributes:
- type (:obj:`str`): Type of the result.
- id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
+
+ Note:
+ All URLs passed in inline query results will be available to end users and therefore must
+ be assumed to be *public*.
Args:
type (:obj:`str`): Type of the result.
id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): Type of the result.
+ id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
+
"""
- def __init__(self, type, id, **kwargs):
+ __slots__ = ('type', 'id', '_id_attrs')
+
+ def __init__(self, type: str, id: str, **_kwargs: Any):
# Required
self.type = str(type)
- self.id = str(id)
+ self.id = str(id) # pylint: disable=C0103
self._id_attrs = (self.id,)
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
+
+ # pylint: disable=E1101
+ if (
+ hasattr(self, 'caption_entities')
+ and self.caption_entities # type: ignore[attr-defined]
+ ):
+ data['caption_entities'] = [
+ ce.to_dict() for ce in self.caption_entities # type: ignore[attr-defined]
+ ]
+
+ return data
diff --git a/telegramer/include/telegram/inline/inlinequeryresultarticle.py b/telegramer/include/telegram/inline/inlinequeryresultarticle.py
index 040dad9..ecc975c 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultarticle.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultarticle.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,28 +18,17 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultArticle."""
+from typing import TYPE_CHECKING, Any
+
from telegram import InlineQueryResult
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
+
class InlineQueryResultArticle(InlineQueryResult):
"""This object represents a Telegram InlineQueryResultArticle.
- Attributes:
- type (:obj:`str`): 'article'.
- id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
- title (:obj:`str`): Title of the result.
- input_message_content (:class:`telegram.InputMessageContent`): Content of the message to
- be sent.
- reply_markup (:class:`telegram.ReplyMarkup`): Optional. Inline keyboard attached to
- the message.
- url (:obj:`str`): Optional. URL of the result.
- hide_url (:obj:`bool`): Optional. Pass True, if you don't want the URL to be shown in the
- message.
- description (:obj:`str`): Optional. Short description of the result.
- thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result.
- thumb_width (:obj:`int`): Optional. Thumbnail width.
- thumb_height (:obj:`int`): Optional. Thumbnail height.
-
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
title (:obj:`str`): Title of the result.
@@ -48,46 +37,69 @@ class InlineQueryResultArticle(InlineQueryResult):
reply_markup (:class:`telegram.ReplyMarkup`, optional): Inline keyboard attached to
the message
url (:obj:`str`, optional): URL of the result.
- hide_url (:obj:`bool`, optional): Pass True, if you don't want the URL to be shown in the
- message.
+ hide_url (:obj:`bool`, optional): Pass :obj:`True`, if you don't want the URL to be shown
+ in the message.
description (:obj:`str`, optional): Short description of the result.
thumb_url (:obj:`str`, optional): Url of the thumbnail for the result.
thumb_width (:obj:`int`, optional): Thumbnail width.
thumb_height (:obj:`int`, optional): Thumbnail height.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): 'article'.
+ id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
+ title (:obj:`str`): Title of the result.
+ input_message_content (:class:`telegram.InputMessageContent`): Content of the message to
+ be sent.
+ reply_markup (:class:`telegram.ReplyMarkup`): Optional. Inline keyboard attached to
+ the message.
+ url (:obj:`str`): Optional. URL of the result.
+ hide_url (:obj:`bool`): Optional. Pass :obj:`True`, if you don't want the URL to be shown
+ in the message.
+ description (:obj:`str`): Optional. Short description of the result.
+ thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result.
+ thumb_width (:obj:`int`): Optional. Thumbnail width.
+ thumb_height (:obj:`int`): Optional. Thumbnail height.
+
"""
- def __init__(self,
- id,
- title,
- input_message_content,
- reply_markup=None,
- url=None,
- hide_url=None,
- description=None,
- thumb_url=None,
- thumb_width=None,
- thumb_height=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'thumb_width',
+ 'thumb_height',
+ 'hide_url',
+ 'url',
+ 'title',
+ 'description',
+ 'input_message_content',
+ 'thumb_url',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ title: str,
+ input_message_content: 'InputMessageContent',
+ reply_markup: 'ReplyMarkup' = None,
+ url: str = None,
+ hide_url: bool = None,
+ description: str = None,
+ thumb_url: str = None,
+ thumb_width: int = None,
+ thumb_height: int = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultArticle, self).__init__('article', id)
+ super().__init__('article', id)
self.title = title
self.input_message_content = input_message_content
# Optional
- if reply_markup:
- self.reply_markup = reply_markup
- if url:
- self.url = url
- if hide_url:
- self.hide_url = hide_url
- if description:
- self.description = description
- if thumb_url:
- self.thumb_url = thumb_url
- if thumb_width:
- self.thumb_width = thumb_width
- if thumb_height:
- self.thumb_height = thumb_height
+ self.reply_markup = reply_markup
+ self.url = url
+ self.hide_url = hide_url
+ self.description = description
+ self.thumb_url = thumb_url
+ self.thumb_width = thumb_width
+ self.thumb_height = thumb_height
diff --git a/telegramer/include/telegram/inline/inlinequeryresultaudio.py b/telegramer/include/telegram/inline/inlinequeryresultaudio.py
index 0bd2c3c..9f06c62 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultaudio.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultaudio.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultAudio."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultAudio(InlineQueryResult):
@@ -27,67 +34,83 @@ class InlineQueryResultAudio(InlineQueryResult):
Alternatively, you can use :attr:`input_message_content` to send a message with the specified
content instead of the audio.
- Attributes:
- type (:obj:`str`): 'audio'.
+ Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_url (:obj:`str`): A valid URL for the audio file.
title (:obj:`str`): Title.
- performer (:obj:`str`): Optional. Caption, 0-200 characters.
- audio_duration (:obj:`str`): Optional. Performer.
- caption (:obj:`str`): Optional. Audio duration in seconds.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ performer (:obj:`str`, optional): Performer.
+ audio_duration (:obj:`str`, optional): Audio duration in seconds.
+ caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the audio.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
+ type (:obj:`str`): 'audio'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_url (:obj:`str`): A valid URL for the audio file.
title (:obj:`str`): Title.
- performer (:obj:`str`, optional): Caption, 0-200 characters.
- audio_duration (:obj:`str`, optional): Performer.
- caption (:obj:`str`, optional): Audio duration in seconds.
- parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ performer (:obj:`str`): Optional. Performer.
+ audio_duration (:obj:`str`): Optional. Audio duration in seconds.
+ caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the audio.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- id,
- audio_url,
- title,
- performer=None,
- audio_duration=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'caption',
+ 'title',
+ 'parse_mode',
+ 'audio_url',
+ 'performer',
+ 'input_message_content',
+ 'audio_duration',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ audio_url: str,
+ title: str,
+ performer: str = None,
+ audio_duration: int = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultAudio, self).__init__('audio', id)
+ super().__init__('audio', id)
self.audio_url = audio_url
self.title = title
# Optionals
- if performer:
- self.performer = performer
- if audio_duration:
- self.audio_duration = audio_duration
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.performer = performer
+ self.audio_duration = audio_duration
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedaudio.py b/telegramer/include/telegram/inline/inlinequeryresultcachedaudio.py
index 221616f..f92096a 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultcachedaudio.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultcachedaudio.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,61 +18,83 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedAudio."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultCachedAudio(InlineQueryResult):
"""
Represents a link to an mp3 audio file stored on the Telegram servers. By default, this audio
file will be sent by the user. Alternatively, you can use :attr:`input_message_content` to
- send amessage with the specified content instead of the audio.
+ send a message with the specified content instead of the audio.
- Attributes:
- type (:obj:`str`): 'audio'.
+ Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_file_id (:obj:`str`): A valid file identifier for the audio file.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the audio.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
+ type (:obj:`str`): 'audio'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_file_id (:obj:`str`): A valid file identifier for the audio file.
- caption (:obj:`str`, optional): Caption, 0-200 characters
- parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the audio.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- id,
- audio_file_id,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'caption',
+ 'parse_mode',
+ 'audio_file_id',
+ 'input_message_content',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ audio_file_id: str,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultCachedAudio, self).__init__('audio', id)
+ super().__init__('audio', id)
self.audio_file_id = audio_file_id
# Optionals
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultcacheddocument.py b/telegramer/include/telegram/inline/inlinequeryresultcacheddocument.py
index 49816b5..4b76b74 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultcacheddocument.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultcacheddocument.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,9 +16,17 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=W0622
"""This module contains the classes that represent Telegram InlineQueryResultCachedDocument."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultCachedDocument(InlineQueryResult):
@@ -27,61 +35,79 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
by the user with an optional caption. Alternatively, you can use :attr:`input_message_content`
to send a message with the specified content instead of the file.
- Attributes:
- type (:obj:`str`): 'document'.
+ Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
title (:obj:`str`): Title for the result.
document_file_id (:obj:`str`): A valid file identifier for the file.
- description (:obj:`str`): Optional. Short description of the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ description (:obj:`str`, optional): Short description of the result.
+ caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the file.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
+ type (:obj:`str`): 'document'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
title (:obj:`str`): Title for the result.
document_file_id (:obj:`str`): A valid file identifier for the file.
- description (:obj:`str`, optional): Short description of the result.
- caption (:obj:`str`, optional): Caption, 0-200 characters
- parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ description (:obj:`str`): Optional. Short description of the result.
+ caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the file.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- id,
- title,
- document_file_id,
- description=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'document_file_id',
+ 'caption',
+ 'title',
+ 'description',
+ 'parse_mode',
+ 'input_message_content',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ title: str,
+ document_file_id: str,
+ description: str = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultCachedDocument, self).__init__('document', id)
+ super().__init__('document', id)
self.title = title
self.document_file_id = document_file_id
# Optionals
- if description:
- self.description = description
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.description = description
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedgif.py b/telegramer/include/telegram/inline/inlinequeryresultcachedgif.py
index dd01b33..8cd3ad7 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultcachedgif.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultcachedgif.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedGif."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultCachedGif(InlineQueryResult):
@@ -28,57 +35,74 @@ class InlineQueryResultCachedGif(InlineQueryResult):
use :attr:`input_message_content` to send a message with specified content instead of
the animation.
- Attributes:
- type (:obj:`str`): 'gif'.
+ Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
gif_file_id (:obj:`str`): A valid file identifier for the GIF file.
- title (:obj:`str`): Optional. Title for the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional):
+ caption (:obj:`str`, optional): Caption of the GIF file to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the gif.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
+ type (:obj:`str`): 'gif'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
gif_file_id (:obj:`str`): A valid file identifier for the GIF file.
- title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional):
- caption (:obj:`str`, optional): Caption, 0-200 characters
- parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ title (:obj:`str`): Optional. Title for the result.
+ caption (:obj:`str`): Optional. Caption of the GIF file to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the gif.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- id,
- gif_file_id,
- title=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'caption',
+ 'title',
+ 'input_message_content',
+ 'parse_mode',
+ 'gif_file_id',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ gif_file_id: str,
+ title: str = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultCachedGif, self).__init__('gif', id)
+ super().__init__('gif', id)
self.gif_file_id = gif_file_id
# Optionals
- if title:
- self.title = title
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.title = title
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedmpeg4gif.py b/telegramer/include/telegram/inline/inlinequeryresultcachedmpeg4gif.py
index 41f099e..8f1d457 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultcachedmpeg4gif.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultcachedmpeg4gif.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
@@ -28,57 +35,74 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
optional caption. Alternatively, you can use :attr:`input_message_content` to send a message
with the specified content instead of the animation.
- Attributes:
- type (:obj:`str`): 'mpeg4_gif'.
+ Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
mpeg4_file_id (:obj:`str`): A valid file identifier for the MP4 file.
- title (:obj:`str`): Optional. Title for the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
- parse_mode (:obj:`str`): Send Markdown or HTML, if you want Telegram apps to show
+ title (:obj:`str`, optional): Title for the result.
+ caption (:obj:`str`, optional): Caption of the MPEG-4 file to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the MPEG-4 file.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
+ type (:obj:`str`): 'mpeg4_gif'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
mpeg4_file_id (:obj:`str`): A valid file identifier for the MP4 file.
- title (:obj:`str`, optional): Title for the result.
- caption (:obj:`str`, optional): Caption, 0-200 characters
- parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ title (:obj:`str`): Optional. Title for the result.
+ caption (:obj:`str`): Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the MPEG-4 file.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- id,
- mpeg4_file_id,
- title=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'mpeg4_file_id',
+ 'caption',
+ 'title',
+ 'parse_mode',
+ 'input_message_content',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ mpeg4_file_id: str,
+ title: str = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultCachedMpeg4Gif, self).__init__('mpeg4_gif', id)
+ super().__init__('mpeg4_gif', id)
self.mpeg4_file_id = mpeg4_file_id
# Optionals
- if title:
- self.title = title
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.title = title
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedphoto.py b/telegramer/include/telegram/inline/inlinequeryresultcachedphoto.py
index 89b8dad..8bba69b 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultcachedphoto.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultcachedphoto.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,9 +16,17 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=W0622
"""This module contains the classes that represent Telegram InlineQueryResultPhoto"""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultCachedPhoto(InlineQueryResult):
@@ -28,62 +36,79 @@ class InlineQueryResultCachedPhoto(InlineQueryResult):
:attr:`input_message_content` to send a message with the specified content instead
of the photo.
- Attributes:
- type (:obj:`str`): 'photo'.
- id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
- photo_file_id (:obj:`str`): A valid file identifier of the photo.
- title (:obj:`str`): Optional. Title for the result.
- description (:obj:`str`): Optional. Short description of the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the photo.
-
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
photo_file_id (:obj:`str`): A valid file identifier of the photo.
title (:obj:`str`, optional): Title for the result.
description (:obj:`str`, optional): Short description of the result.
- caption (:obj:`str`, optional): Caption, 0-200 characters
+ caption (:obj:`str`, optional): Caption of the photo to be sent, 0-1024 characters after
+ entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the photo.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): 'photo'.
+ id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
+ photo_file_id (:obj:`str`): A valid file identifier of the photo.
+ title (:obj:`str`): Optional. Title for the result.
+ description (:obj:`str`): Optional. Short description of the result.
+ caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-1024 characters after
+ entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ bold, italic, fixed-width text or inline URLs in the media caption. See the constants
+ in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ message to be sent instead of the photo.
+
"""
- def __init__(self,
- id,
- photo_file_id,
- title=None,
- description=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'caption',
+ 'title',
+ 'description',
+ 'parse_mode',
+ 'photo_file_id',
+ 'input_message_content',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ photo_file_id: str,
+ title: str = None,
+ description: str = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultCachedPhoto, self).__init__('photo', id)
+ super().__init__('photo', id)
self.photo_file_id = photo_file_id
# Optionals
- if title:
- self.title = title
- if description:
- self.description = description
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.title = title
+ self.description = description
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedsticker.py b/telegramer/include/telegram/inline/inlinequeryresultcachedsticker.py
index d9ad8df..0425fa5 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultcachedsticker.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultcachedsticker.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,8 +18,13 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedSticker."""
+from typing import TYPE_CHECKING, Any
+
from telegram import InlineQueryResult
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
+
class InlineQueryResultCachedSticker(InlineQueryResult):
"""
@@ -27,6 +32,15 @@ class InlineQueryResultCachedSticker(InlineQueryResult):
be sent by the user. Alternatively, you can use :attr:`input_message_content` to send a
message with the specified content instead of the sticker.
+ Args:
+ id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
+ sticker_file_id (:obj:`str`): A valid file identifier of the sticker.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
+ message to be sent instead of the sticker.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
Attributes:
type (:obj:`str`): 'sticker`.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
@@ -36,29 +50,22 @@ class InlineQueryResultCachedSticker(InlineQueryResult):
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the sticker.
- Args:
- id (:obj:`str`):
- sticker_file_id (:obj:`str`):
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
- message to be sent instead of the sticker.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
-
"""
- def __init__(self,
- id,
- sticker_file_id,
- reply_markup=None,
- input_message_content=None,
- **kwargs):
+ __slots__ = ('reply_markup', 'input_message_content', 'sticker_file_id')
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ sticker_file_id: str,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultCachedSticker, self).__init__('sticker', id)
+ super().__init__('sticker', id)
self.sticker_file_id = sticker_file_id
# Optionals
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedvideo.py b/telegramer/include/telegram/inline/inlinequeryresultcachedvideo.py
index 75cd675..3283794 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultcachedvideo.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultcachedvideo.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedVideo."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultCachedVideo(InlineQueryResult):
@@ -28,61 +35,79 @@ class InlineQueryResultCachedVideo(InlineQueryResult):
:attr:`input_message_content` to send a message with the specified content instead
of the video.
- Attributes:
- type (:obj:`str`): 'video'.
+ Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
video_file_id (:obj:`str`): A valid file identifier for the video file.
title (:obj:`str`): Title for the result.
- description (:obj:`str`): Optional. Short description of the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ description (:obj:`str`, optional): Short description of the result.
+ caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters after
+ entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the video.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
+ type (:obj:`str`): 'video'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
video_file_id (:obj:`str`): A valid file identifier for the video file.
title (:obj:`str`): Title for the result.
- description (:obj:`str`, optional): Short description of the result.
- caption (:obj:`str`, optional): Caption, 0-200 characters.
- parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ description (:obj:`str`): Optional. Short description of the result.
+ caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters after
+ entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the video.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- id,
- video_file_id,
- title,
- description=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'caption',
+ 'title',
+ 'description',
+ 'parse_mode',
+ 'input_message_content',
+ 'video_file_id',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ video_file_id: str,
+ title: str,
+ description: str = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultCachedVideo, self).__init__('video', id)
+ super().__init__('video', id)
self.video_file_id = video_file_id
self.title = title
# Optionals
- if description:
- self.description = description
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.description = description
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultcachedvoice.py b/telegramer/include/telegram/inline/inlinequeryresultcachedvoice.py
index 503325c..f5ff9bf 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultcachedvoice.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultcachedvoice.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedVoice."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultCachedVoice(InlineQueryResult):
@@ -27,56 +34,72 @@ class InlineQueryResultCachedVoice(InlineQueryResult):
message will be sent by the user. Alternatively, you can use :attr:`input_message_content` to
send a message with the specified content instead of the voice message.
- Attributes:
- type (:obj:`str`): 'voice'.
+ Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
voice_file_id (:obj:`str`): A valid file identifier for the voice message.
title (:obj:`str`): Voice message title.
- caption (:obj:`str`): Optional. Caption, 0-200 characters.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the voice.
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
+ message to be sent instead of the voice message.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
+ type (:obj:`str`): 'voice'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
voice_file_id (:obj:`str`): A valid file identifier for the voice message.
title (:obj:`str`): Voice message title.
- caption (:obj:`str`, optional): Caption, 0-200 characters.
- parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
- message to be sent instead of the voice.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ message to be sent instead of the voice message.
"""
- def __init__(self,
- id,
- voice_file_id,
- title,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'caption',
+ 'title',
+ 'parse_mode',
+ 'voice_file_id',
+ 'input_message_content',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ voice_file_id: str,
+ title: str,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultCachedVoice, self).__init__('voice', id)
+ super().__init__('voice', id)
self.voice_file_id = voice_file_id
self.title = title
# Optionals
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultcontact.py b/telegramer/include/telegram/inline/inlinequeryresultcontact.py
index 2963cfc..df4f6c9 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultcontact.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultcontact.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,8 +18,13 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultContact."""
+from typing import TYPE_CHECKING, Any
+
from telegram import InlineQueryResult
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
+
class InlineQueryResultContact(InlineQueryResult):
"""
@@ -27,22 +32,6 @@ class InlineQueryResultContact(InlineQueryResult):
Alternatively, you can use :attr:`input_message_content` to send a message with the specified
content instead of the contact.
- Attributes:
- type (:obj:`str`): 'contact'.
- id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
- phone_number (:obj:`str`): Contact's phone number.
- first_name (:obj:`str`): Contact's first name.
- last_name (:obj:`str`): Optional. Contact's last name.
- vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard,
- 0-2048 bytes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the contact.
- thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result.
- thumb_width (:obj:`int`): Optional. Thumbnail width.
- thumb_height (:obj:`int`): Optional. Thumbnail height.
-
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
phone_number (:obj:`str`): Contact's phone number.
@@ -59,37 +48,60 @@ class InlineQueryResultContact(InlineQueryResult):
thumb_height (:obj:`int`, optional): Thumbnail height.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): 'contact'.
+ id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
+ phone_number (:obj:`str`): Contact's phone number.
+ first_name (:obj:`str`): Contact's first name.
+ last_name (:obj:`str`): Optional. Contact's last name.
+ vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard,
+ 0-2048 bytes.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ message to be sent instead of the contact.
+ thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result.
+ thumb_width (:obj:`int`): Optional. Thumbnail width.
+ thumb_height (:obj:`int`): Optional. Thumbnail height.
+
"""
- def __init__(self,
- id,
- phone_number,
- first_name,
- last_name=None,
- reply_markup=None,
- input_message_content=None,
- thumb_url=None,
- thumb_width=None,
- thumb_height=None,
- vcard=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'thumb_width',
+ 'thumb_height',
+ 'vcard',
+ 'first_name',
+ 'last_name',
+ 'phone_number',
+ 'input_message_content',
+ 'thumb_url',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ phone_number: str,
+ first_name: str,
+ last_name: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ thumb_url: str = None,
+ thumb_width: int = None,
+ thumb_height: int = None,
+ vcard: str = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultContact, self).__init__('contact', id)
+ super().__init__('contact', id)
self.phone_number = phone_number
self.first_name = first_name
# Optionals
- if last_name:
- self.last_name = last_name
- if vcard:
- self.vcard = vcard
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
- if thumb_url:
- self.thumb_url = thumb_url
- if thumb_width:
- self.thumb_width = thumb_width
- if thumb_height:
- self.thumb_height = thumb_height
+ self.last_name = last_name
+ self.vcard = vcard
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
+ self.thumb_url = thumb_url
+ self.thumb_width = thumb_width
+ self.thumb_height = thumb_height
diff --git a/telegramer/include/telegram/inline/inlinequeryresultdocument.py b/telegramer/include/telegram/inline/inlinequeryresultdocument.py
index a4584a9..42555aa 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultdocument.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultdocument.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultDocument"""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultDocument(InlineQueryResult):
@@ -28,82 +35,101 @@ class InlineQueryResultDocument(InlineQueryResult):
specified content instead of the file. Currently, only .PDF and .ZIP files can be sent
using this method.
- Attributes:
- type (:obj:`str`): 'document'.
+ Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
title (:obj:`str`): Title for the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
document_url (:obj:`str`): A valid URL for the file.
mime_type (:obj:`str`): Mime type of the content of the file, either "application/pdf"
or "application/zip".
- description (:obj:`str`): Optional. Short description of the result.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ description (:obj:`str`, optional): Short description of the result.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the file.
- thumb_url (:obj:`str`): Optional. URL of the thumbnail (jpeg only) for the file.
- thumb_width (:obj:`int`): Optional. Thumbnail width.
- thumb_height (:obj:`int`): Optional. Thumbnail height.
+ thumb_url (:obj:`str`, optional): URL of the thumbnail (jpeg only) for the file.
+ thumb_width (:obj:`int`, optional): Thumbnail width.
+ thumb_height (:obj:`int`, optional): Thumbnail height.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
+ type (:obj:`str`): 'document'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
title (:obj:`str`): Title for the result.
- caption (:obj:`str`, optional): Caption, 0-200 characters
- parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
document_url (:obj:`str`): A valid URL for the file.
mime_type (:obj:`str`): Mime type of the content of the file, either "application/pdf"
or "application/zip".
- description (:obj:`str`, optional): Short description of the result.
+ description (:obj:`str`): Optional. Short description of the result.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the file.
- thumb_url (:obj:`str`, optional): URL of the thumbnail (jpeg only) for the file.
- thumb_width (:obj:`int`, optional): Thumbnail width.
- thumb_height (:obj:`int`, optional): Thumbnail height.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ thumb_url (:obj:`str`): Optional. URL of the thumbnail (jpeg only) for the file.
+ thumb_width (:obj:`int`): Optional. Thumbnail width.
+ thumb_height (:obj:`int`): Optional. Thumbnail height.
"""
- def __init__(self,
- id,
- document_url,
- title,
- mime_type,
- caption=None,
- description=None,
- reply_markup=None,
- input_message_content=None,
- thumb_url=None,
- thumb_width=None,
- thumb_height=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'document_url',
+ 'thumb_width',
+ 'thumb_height',
+ 'caption',
+ 'title',
+ 'description',
+ 'parse_mode',
+ 'mime_type',
+ 'thumb_url',
+ 'input_message_content',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ document_url: str,
+ title: str,
+ mime_type: str,
+ caption: str = None,
+ description: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ thumb_url: str = None,
+ thumb_width: int = None,
+ thumb_height: int = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultDocument, self).__init__('document', id)
+ super().__init__('document', id)
self.document_url = document_url
self.title = title
self.mime_type = mime_type
# Optionals
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if description:
- self.description = description
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
- if thumb_url:
- self.thumb_url = thumb_url
- if thumb_width:
- self.thumb_width = thumb_width
- if thumb_height:
- self.thumb_height = thumb_height
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.description = description
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
+ self.thumb_url = thumb_url
+ self.thumb_width = thumb_width
+ self.thumb_height = thumb_height
diff --git a/telegramer/include/telegram/inline/inlinequeryresultgame.py b/telegramer/include/telegram/inline/inlinequeryresultgame.py
index cc06b76..02e4a7d 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultgame.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultgame.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,33 +18,45 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultGame."""
+from typing import TYPE_CHECKING, Any
+
from telegram import InlineQueryResult
+if TYPE_CHECKING:
+ from telegram import ReplyMarkup
+
class InlineQueryResultGame(InlineQueryResult):
- """Represents a Game.
+ """Represents a :class:`telegram.Game`.
- Attributes:
- type (:obj:`str`): 'game'.
+ Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
game_short_name (:obj:`str`): Short name of the game.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
+ type (:obj:`str`): 'game'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
game_short_name (:obj:`str`): Short name of the game.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self, id, game_short_name, reply_markup=None, **kwargs):
+ __slots__ = ('reply_markup', 'game_short_name')
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ game_short_name: str,
+ reply_markup: 'ReplyMarkup' = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultGame, self).__init__('game', id)
- self.id = id
+ super().__init__('game', id)
+ self.id = id # pylint: disable=W0622
self.game_short_name = game_short_name
- if reply_markup:
- self.reply_markup = reply_markup
+ self.reply_markup = reply_markup
diff --git a/telegramer/include/telegram/inline/inlinequeryresultgif.py b/telegramer/include/telegram/inline/inlinequeryresultgif.py
index d8b62c6..e879dc0 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultgif.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultgif.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,9 +16,17 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=W0622
"""This module contains the classes that represent Telegram InlineQueryResultGif."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultGif(InlineQueryResult):
@@ -27,6 +35,31 @@ class InlineQueryResultGif(InlineQueryResult):
the user with optional caption. Alternatively, you can use :attr:`input_message_content` to
send a message with the specified content instead of the animation.
+ Args:
+ id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
+ gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB.
+ gif_width (:obj:`int`, optional): Width of the GIF.
+ gif_height (:obj:`int`, optional): Height of the GIF.
+ gif_duration (:obj:`int`, optional): Duration of the GIF
+ thumb_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for
+ the result.
+ thumb_mime_type (:obj:`str`, optional): MIME type of the thumbnail, must be one of
+ ``'image/jpeg'``, ``'image/gif'``, or ``'video/mp4'``. Defaults to ``'image/jpeg'``.
+ title (:obj:`str`, optional): Title for the result.
+ caption (:obj:`str`, optional): Caption of the GIF file to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
+ bold, italic, fixed-width text or inline URLs in the media caption. See the constants
+ in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
+ message to be sent instead of the GIF animation.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
Attributes:
type (:obj:`str`): 'gif'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
@@ -34,70 +67,71 @@ class InlineQueryResultGif(InlineQueryResult):
gif_width (:obj:`int`): Optional. Width of the GIF.
gif_height (:obj:`int`): Optional. Height of the GIF.
gif_duration (:obj:`int`): Optional. Duration of the GIF.
- thumb_url (:obj:`str`): URL of the static thumbnail for the result (jpeg or gif).
+ thumb_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for
+ the result.
+ thumb_mime_type (:obj:`str`): Optional. MIME type of the thumbnail.
title (:obj:`str`): Optional. Title for the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
+ caption (:obj:`str`): Optional. Caption of the GIF file to be sent, 0-1024 characters
+ after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the gif.
-
- Args:
- id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
- gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB.
- gif_width (:obj:`int`, optional): Width of the GIF.
- gif_height (:obj:`int`, optional): Height of the GIF.
- gif_duration (:obj:`int`, optional): Duration of the GIF
- thumb_url (:obj:`str`): URL of the static thumbnail for the result (jpeg or gif).
- title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional):
- caption (:obj:`str`, optional): Caption, 0-200 characters
- parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
- message to be sent instead of the gif.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ message to be sent instead of the GIF animation.
"""
- def __init__(self,
- id,
- gif_url,
- thumb_url,
- gif_width=None,
- gif_height=None,
- title=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- gif_duration=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'gif_height',
+ 'thumb_mime_type',
+ 'caption_entities',
+ 'gif_width',
+ 'title',
+ 'caption',
+ 'parse_mode',
+ 'gif_duration',
+ 'input_message_content',
+ 'gif_url',
+ 'thumb_url',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ gif_url: str,
+ thumb_url: str,
+ gif_width: int = None,
+ gif_height: int = None,
+ title: str = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ gif_duration: int = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb_mime_type: str = None,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultGif, self).__init__('gif', id)
+ super().__init__('gif', id)
self.gif_url = gif_url
self.thumb_url = thumb_url
# Optionals
- if gif_width:
- self.gif_width = gif_width
- if gif_height:
- self.gif_height = gif_height
- if gif_duration:
- self.gif_duration = gif_duration
- if title:
- self.title = title
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.gif_width = gif_width
+ self.gif_height = gif_height
+ self.gif_duration = gif_duration
+ self.title = title
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
+ self.thumb_mime_type = thumb_mime_type
diff --git a/telegramer/include/telegram/inline/inlinequeryresultlocation.py b/telegramer/include/telegram/inline/inlinequeryresultlocation.py
index e30c271..e55d90b 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultlocation.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultlocation.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,8 +18,13 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultLocation."""
+from typing import TYPE_CHECKING, Any
+
from telegram import InlineQueryResult
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
+
class InlineQueryResultLocation(InlineQueryResult):
"""
@@ -27,29 +32,20 @@ class InlineQueryResultLocation(InlineQueryResult):
Alternatively, you can use :attr:`input_message_content` to send a message with the specified
content instead of the location.
- Attributes:
- type (:obj:`str`): 'location'.
- id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
- latitude (:obj:`float`): Location latitude in degrees.
- longitude (:obj:`float`): Location longitude in degrees.
- title (:obj:`str`): Location title.
- live_period (:obj:`int`): Optional. Period in seconds for which the location can be
- updated, should be between 60 and 86400.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the location.
- thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result.
- thumb_width (:obj:`int`): Optional. Thumbnail width.
- thumb_height (:obj:`int`): Optional. Thumbnail height.
-
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
latitude (:obj:`float`): Location latitude in degrees.
longitude (:obj:`float`): Location longitude in degrees.
title (:obj:`str`): Location title.
+ horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location,
+ measured in meters; 0-1500.
live_period (:obj:`int`, optional): Period in seconds for which the location can be
updated, should be between 60 and 86400.
+ heading (:obj:`int`, optional): For live locations, a direction in which the user is
+ moving, in degrees. Must be between 1 and 360 if specified.
+ proximity_alert_radius (:obj:`int`, optional): For live locations, a maximum distance for
+ proximity alerts about approaching another chat member, in meters. Must be between 1
+ and 100000 if specified.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
@@ -59,36 +55,77 @@ class InlineQueryResultLocation(InlineQueryResult):
thumb_height (:obj:`int`, optional): Thumbnail height.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): 'location'.
+ id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
+ latitude (:obj:`float`): Location latitude in degrees.
+ longitude (:obj:`float`): Location longitude in degrees.
+ title (:obj:`str`): Location title.
+ horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location,
+ measured in meters.
+ live_period (:obj:`int`): Optional. Period in seconds for which the location can be
+ updated, should be between 60 and 86400.
+ heading (:obj:`int`): Optional. For live locations, a direction in which the user is
+ moving, in degrees.
+ proximity_alert_radius (:obj:`int`): Optional. For live locations, a maximum distance for
+ proximity alerts about approaching another chat member, in meters.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ message to be sent instead of the location.
+ thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result.
+ thumb_width (:obj:`int`): Optional. Thumbnail width.
+ thumb_height (:obj:`int`): Optional. Thumbnail height.
+
"""
- def __init__(self,
- id,
- latitude,
- longitude,
- title,
- live_period=None,
- reply_markup=None,
- input_message_content=None,
- thumb_url=None,
- thumb_width=None,
- thumb_height=None,
- **kwargs):
+ __slots__ = (
+ 'longitude',
+ 'reply_markup',
+ 'thumb_width',
+ 'thumb_height',
+ 'heading',
+ 'title',
+ 'live_period',
+ 'proximity_alert_radius',
+ 'input_message_content',
+ 'latitude',
+ 'horizontal_accuracy',
+ 'thumb_url',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ latitude: float,
+ longitude: float,
+ title: str,
+ live_period: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ thumb_url: str = None,
+ thumb_width: int = None,
+ thumb_height: int = None,
+ horizontal_accuracy: float = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultLocation, self).__init__('location', id)
- self.latitude = latitude
- self.longitude = longitude
+ super().__init__('location', id)
+ self.latitude = float(latitude)
+ self.longitude = float(longitude)
self.title = title
# Optionals
- if live_period:
- self.live_period = live_period
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
- if thumb_url:
- self.thumb_url = thumb_url
- if thumb_width:
- self.thumb_width = thumb_width
- if thumb_height:
- self.thumb_height = thumb_height
+ self.live_period = live_period
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
+ self.thumb_url = thumb_url
+ self.thumb_width = thumb_width
+ self.thumb_height = thumb_height
+ self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None
+ self.heading = int(heading) if heading else None
+ self.proximity_alert_radius = (
+ int(proximity_alert_radius) if proximity_alert_radius else None
+ )
diff --git a/telegramer/include/telegram/inline/inlinequeryresultmpeg4gif.py b/telegramer/include/telegram/inline/inlinequeryresultmpeg4gif.py
index 3459d77..5f73218 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultmpeg4gif.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultmpeg4gif.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultMpeg4Gif(InlineQueryResult):
@@ -28,24 +35,6 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
use :attr:`input_message_content` to send a message with the specified content instead of the
animation.
- Attributes:
- type (:obj:`str`): 'mpeg4_gif'.
- id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
- mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB.
- mpeg4_width (:obj:`int`): Optional. Video width.
- mpeg4_height (:obj:`int`): Optional. Video height.
- mpeg4_duration (:obj:`int`): Optional. Video duration.
- thumb_url (:obj:`str`): URL of the static thumbnail (jpeg or gif) for the result.
- title (:obj:`str`): Optional. Title for the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the MPEG-4 file.
-
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB.
@@ -53,52 +42,95 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
mpeg4_height (:obj:`int`, optional): Video height.
mpeg4_duration (:obj:`int`, optional): Video duration.
thumb_url (:obj:`str`): URL of the static thumbnail (jpeg or gif) for the result.
+ thumb_mime_type (:obj:`str`): Optional. MIME type of the thumbnail, must be one of
+ ``'image/jpeg'``, ``'image/gif'``, or ``'video/mp4'``. Defaults to ``'image/jpeg'``.
title (:obj:`str`, optional): Title for the result.
- caption (:obj:`str`, optional): Caption, 0-200 characters
+ caption (:obj:`str`, optional): Caption of the MPEG-4 file to be sent, 0-1024 characters
+ after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
- message to be sent instead of the MPEG-4 file.
+ message to be sent instead of the video animation.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): 'mpeg4_gif'.
+ id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
+ mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB.
+ mpeg4_width (:obj:`int`): Optional. Video width.
+ mpeg4_height (:obj:`int`): Optional. Video height.
+ mpeg4_duration (:obj:`int`): Optional. Video duration.
+ thumb_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for
+ the result.
+ thumb_mime_type (:obj:`str`): Optional. MIME type of the thumbnail.
+ title (:obj:`str`): Optional. Title for the result.
+ caption (:obj:`str`): Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters
+ after entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ bold, italic, fixed-width text or inline URLs in the media caption. See the constants
+ in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ message to be sent instead of the video animation.
+
"""
- def __init__(self,
- id,
- mpeg4_url,
- thumb_url,
- mpeg4_width=None,
- mpeg4_height=None,
- title=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- mpeg4_duration=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'thumb_mime_type',
+ 'caption_entities',
+ 'mpeg4_duration',
+ 'mpeg4_width',
+ 'title',
+ 'caption',
+ 'parse_mode',
+ 'input_message_content',
+ 'mpeg4_url',
+ 'mpeg4_height',
+ 'thumb_url',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ mpeg4_url: str,
+ thumb_url: str,
+ mpeg4_width: int = None,
+ mpeg4_height: int = None,
+ title: str = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ mpeg4_duration: int = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb_mime_type: str = None,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultMpeg4Gif, self).__init__('mpeg4_gif', id)
+ super().__init__('mpeg4_gif', id)
self.mpeg4_url = mpeg4_url
self.thumb_url = thumb_url
# Optional
- if mpeg4_width:
- self.mpeg4_width = mpeg4_width
- if mpeg4_height:
- self.mpeg4_height = mpeg4_height
- if mpeg4_duration:
- self.mpeg4_duration = mpeg4_duration
- if title:
- self.title = title
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.mpeg4_width = mpeg4_width
+ self.mpeg4_height = mpeg4_height
+ self.mpeg4_duration = mpeg4_duration
+ self.title = title
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
+ self.thumb_mime_type = thumb_mime_type
diff --git a/telegramer/include/telegram/inline/inlinequeryresultphoto.py b/telegramer/include/telegram/inline/inlinequeryresultphoto.py
index efaf95f..e6de727 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultphoto.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultphoto.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultPhoto."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultPhoto(InlineQueryResult):
@@ -27,25 +34,6 @@ class InlineQueryResultPhoto(InlineQueryResult):
caption. Alternatively, you can use :attr:`input_message_content` to send a message with the
specified content instead of the photo.
- Attributes:
- type (:obj:`str`): 'photo'.
- id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
- photo_url (:obj:`str`): A valid URL of the photo. Photo must be in jpeg format. Photo size
- must not exceed 5MB.
- thumb_url (:obj:`str`): URL of the thumbnail for the photo.
- photo_width (:obj:`int`): Optional. Width of the photo.
- photo_height (:obj:`int`): Optional. Height of the photo.
- title (:obj:`str`): Optional. Title for the result.
- description (:obj:`str`): Optional. Short description of the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the photo.
-
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
photo_url (:obj:`str`): A valid URL of the photo. Photo must be in jpeg format. Photo size
@@ -55,50 +43,87 @@ class InlineQueryResultPhoto(InlineQueryResult):
photo_height (:obj:`int`, optional): Height of the photo.
title (:obj:`str`, optional): Title for the result.
description (:obj:`str`, optional): Short description of the result.
- caption (:obj:`str`, optional): Caption, 0-200 characters
+ caption (:obj:`str`, optional): Caption of the photo to be sent, 0-1024 characters after
+ entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the photo.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): 'photo'.
+ id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
+ photo_url (:obj:`str`): A valid URL of the photo. Photo must be in jpeg format. Photo size
+ must not exceed 5MB.
+ thumb_url (:obj:`str`): URL of the thumbnail for the photo.
+ photo_width (:obj:`int`): Optional. Width of the photo.
+ photo_height (:obj:`int`): Optional. Height of the photo.
+ title (:obj:`str`): Optional. Title for the result.
+ description (:obj:`str`): Optional. Short description of the result.
+ caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-1024 characters after
+ entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ bold, italic, fixed-width text or inline URLs in the media caption. See the constants
+ in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ message to be sent instead of the photo.
+
"""
- def __init__(self,
- id,
- photo_url,
- thumb_url,
- photo_width=None,
- photo_height=None,
- title=None,
- description=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'photo_url',
+ 'reply_markup',
+ 'caption_entities',
+ 'photo_width',
+ 'caption',
+ 'title',
+ 'description',
+ 'parse_mode',
+ 'input_message_content',
+ 'photo_height',
+ 'thumb_url',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ photo_url: str,
+ thumb_url: str,
+ photo_width: int = None,
+ photo_height: int = None,
+ title: str = None,
+ description: str = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultPhoto, self).__init__('photo', id)
+ super().__init__('photo', id)
self.photo_url = photo_url
self.thumb_url = thumb_url
# Optionals
- if photo_width:
- self.photo_width = int(photo_width)
- if photo_height:
- self.photo_height = int(photo_height)
- if title:
- self.title = title
- if description:
- self.description = description
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.photo_width = int(photo_width) if photo_width is not None else None
+ self.photo_height = int(photo_height) if photo_height is not None else None
+ self.title = title
+ self.description = description
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultvenue.py b/telegramer/include/telegram/inline/inlinequeryresultvenue.py
index f64fcc2..4a64a10 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultvenue.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultvenue.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,8 +18,13 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultVenue."""
+from typing import TYPE_CHECKING, Any
+
from telegram import InlineQueryResult
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
+
class InlineQueryResultVenue(InlineQueryResult):
"""
@@ -27,24 +32,9 @@ class InlineQueryResultVenue(InlineQueryResult):
use :attr:`input_message_content` to send a message with the specified content instead of the
venue.
- Attributes:
- type (:obj:`str`): 'venue'.
- id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
- latitude (:obj:`float`): Latitude of the venue location in degrees.
- longitude (:obj:`float`): Longitude of the venue location in degrees.
- title (:obj:`str`): Title of the venue.
- address (:obj:`str`): Address of the venue.
- foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue if known.
- foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known.
- (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or
- "food/icecream".)
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the venue.
- thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result.
- thumb_width (:obj:`int`): Optional. Thumbnail width.
- thumb_height (:obj:`int`): Optional. Thumbnail height.
+ Note:
+ Foursquare details and Google Pace details are mutually exclusive. However, this
+ behaviour is undocumented and might be changed by Telegram.
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
@@ -56,6 +46,9 @@ class InlineQueryResultVenue(InlineQueryResult):
foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known.
(For example, "arts_entertainment/default", "arts_entertainment/aquarium" or
"food/icecream".)
+ google_place_id (:obj:`str`, optional): Google Places identifier of the venue.
+ google_place_type (:obj:`str`, optional): Google Places type of the venue. (See
+ `supported types `_.)
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
@@ -65,42 +58,76 @@ class InlineQueryResultVenue(InlineQueryResult):
thumb_height (:obj:`int`, optional): Thumbnail height.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): 'venue'.
+ id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
+ latitude (:obj:`float`): Latitude of the venue location in degrees.
+ longitude (:obj:`float`): Longitude of the venue location in degrees.
+ title (:obj:`str`): Title of the venue.
+ address (:obj:`str`): Address of the venue.
+ foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue if known.
+ foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known.
+ google_place_id (:obj:`str`): Optional. Google Places identifier of the venue.
+ google_place_type (:obj:`str`): Optional. Google Places type of the venue.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ message to be sent instead of the venue.
+ thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result.
+ thumb_width (:obj:`int`): Optional. Thumbnail width.
+ thumb_height (:obj:`int`): Optional. Thumbnail height.
+
"""
- def __init__(self,
- id,
- latitude,
- longitude,
- title,
- address,
- foursquare_id=None,
- foursquare_type=None,
- reply_markup=None,
- input_message_content=None,
- thumb_url=None,
- thumb_width=None,
- thumb_height=None,
- **kwargs):
+ __slots__ = (
+ 'longitude',
+ 'reply_markup',
+ 'google_place_type',
+ 'thumb_width',
+ 'thumb_height',
+ 'title',
+ 'address',
+ 'foursquare_id',
+ 'foursquare_type',
+ 'google_place_id',
+ 'input_message_content',
+ 'latitude',
+ 'thumb_url',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ latitude: float,
+ longitude: float,
+ title: str,
+ address: str,
+ foursquare_id: str = None,
+ foursquare_type: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ thumb_url: str = None,
+ thumb_width: int = None,
+ thumb_height: int = None,
+ google_place_id: str = None,
+ google_place_type: str = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultVenue, self).__init__('venue', id)
+ super().__init__('venue', id)
self.latitude = latitude
self.longitude = longitude
self.title = title
self.address = address
# Optional
- if foursquare_id:
- self.foursquare_id = foursquare_id
- if foursquare_type:
- self.foursquare_type = foursquare_type
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
- if thumb_url:
- self.thumb_url = thumb_url
- if thumb_width:
- self.thumb_width = thumb_width
- if thumb_height:
- self.thumb_height = thumb_height
+ self.foursquare_id = foursquare_id
+ self.foursquare_type = foursquare_type
+ self.google_place_id = google_place_id
+ self.google_place_type = google_place_type
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
+ self.thumb_url = thumb_url
+ self.thumb_width = thumb_width
+ self.thumb_height = thumb_height
diff --git a/telegramer/include/telegram/inline/inlinequeryresultvideo.py b/telegramer/include/telegram/inline/inlinequeryresultvideo.py
index a21c525..098e3b0 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultvideo.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultvideo.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultVideo."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultVideo(InlineQueryResult):
@@ -28,25 +35,9 @@ class InlineQueryResultVideo(InlineQueryResult):
:attr:`input_message_content` to send a message with the specified content instead of
the video.
- Attributes:
- type (:obj:`str`): 'video'.
- id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
- video_url (:obj:`str`): A valid URL for the embedded video player or video file.
- mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4".
- thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video.
- title (:obj:`str`): Title for the result.
- caption (:obj:`str`): Optional. Caption, 0-200 characters
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- video_width (:obj:`int`): Optional. Video width.
- video_height (:obj:`int`): Optional. Video height.
- video_duration (:obj:`int`): Optional. Video duration in seconds.
- description (:obj:`str`): Optional. Short description of the result.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the video.
+ Note:
+ If an InlineQueryResultVideo message contains an embedded video (e.g., YouTube), you must
+ replace its content using :attr:`input_message_content`.
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
@@ -54,10 +45,13 @@ class InlineQueryResultVideo(InlineQueryResult):
mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4".
thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video.
title (:obj:`str`): Title for the result.
- caption (:obj:`str`, optional): Caption, 0-200 characters.
+ caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
video_width (:obj:`int`, optional): Video width.
video_height (:obj:`int`, optional): Video height.
video_duration (:obj:`int`, optional): Video duration in seconds.
@@ -65,48 +59,88 @@ class InlineQueryResultVideo(InlineQueryResult):
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
- message to be sent instead of the video.
+ message to be sent instead of the video. This field is required if
+ InlineQueryResultVideo is used to send an HTML-page as a result
+ (e.g., a YouTube video).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): 'video'.
+ id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
+ video_url (:obj:`str`): A valid URL for the embedded video player or video file.
+ mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4".
+ thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video.
+ title (:obj:`str`): Title for the result.
+ caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters after
+ entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ bold, italic, fixed-width text or inline URLs in the media caption. See the constants
+ in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ video_width (:obj:`int`): Optional. Video width.
+ video_height (:obj:`int`): Optional. Video height.
+ video_duration (:obj:`int`): Optional. Video duration in seconds.
+ description (:obj:`str`): Optional. Short description of the result.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ message to be sent instead of the video. This field is required if
+ InlineQueryResultVideo is used to send an HTML-page as a result
+ (e.g., a YouTube video).
+
"""
- def __init__(self,
- id,
- video_url,
- mime_type,
- thumb_url,
- title,
- caption=None,
- video_width=None,
- video_height=None,
- video_duration=None,
- description=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'video_url',
+ 'reply_markup',
+ 'caption_entities',
+ 'caption',
+ 'title',
+ 'description',
+ 'video_duration',
+ 'parse_mode',
+ 'mime_type',
+ 'input_message_content',
+ 'video_height',
+ 'video_width',
+ 'thumb_url',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ video_url: str,
+ mime_type: str,
+ thumb_url: str,
+ title: str,
+ caption: str = None,
+ video_width: int = None,
+ video_height: int = None,
+ video_duration: int = None,
+ description: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultVideo, self).__init__('video', id)
+ super().__init__('video', id)
self.video_url = video_url
self.mime_type = mime_type
self.thumb_url = thumb_url
self.title = title
# Optional
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if video_width:
- self.video_width = video_width
- if video_height:
- self.video_height = video_height
- if video_duration:
- self.video_duration = video_duration
- if description:
- self.description = description
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.video_width = video_width
+ self.video_height = video_height
+ self.video_duration = video_duration
+ self.description = description
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inlinequeryresultvoice.py b/telegramer/include/telegram/inline/inlinequeryresultvoice.py
index 817eca4..960f41b 100644
--- a/telegramer/include/telegram/inline/inlinequeryresultvoice.py
+++ b/telegramer/include/telegram/inline/inlinequeryresultvoice.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultVoice."""
-from telegram import InlineQueryResult
+from typing import TYPE_CHECKING, Any, Union, Tuple, List
+
+from telegram import InlineQueryResult, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import ODVInput
+
+if TYPE_CHECKING:
+ from telegram import InputMessageContent, ReplyMarkup
class InlineQueryResultVoice(InlineQueryResult):
@@ -28,62 +35,78 @@ class InlineQueryResultVoice(InlineQueryResult):
:attr:`input_message_content` to send a message with the specified content instead of the
the voice message.
- Attributes:
- type (:obj:`str`): 'voice'.
- id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
- voice_url (:obj:`str`): A valid URL for the voice recording.
- title (:obj:`str`): Voice message title.
- caption (:obj:`str`): Optional. Caption, 0-200 characters.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
- in :class:`telegram.ParseMode` for the available modes.
- voice_duration (:obj:`int`): Optional. Recording duration in seconds.
- reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
- to the message.
- input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
- message to be sent instead of the voice.
-
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
voice_url (:obj:`str`): A valid URL for the voice recording.
- title (:obj:`str`): Voice message title.
- caption (:obj:`str`, optional): Caption, 0-200 characters.
+ title (:obj:`str`): Recording title.
+ caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
+ bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
voice_duration (:obj:`int`, optional): Recording duration in seconds.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
- message to be sent instead of the voice.
+ message to be sent instead of the voice recording.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): 'voice'.
+ id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
+ voice_url (:obj:`str`): A valid URL for the voice recording.
+ title (:obj:`str`): Recording title.
+ caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ bold, italic, fixed-width text or inline URLs in the media caption. See the constants
+ in :class:`telegram.ParseMode` for the available modes.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ voice_duration (:obj:`int`): Optional. Recording duration in seconds.
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ to the message.
+ input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
+ message to be sent instead of the voice recording.
+
"""
- def __init__(self,
- id,
- voice_url,
- title,
- voice_duration=None,
- caption=None,
- reply_markup=None,
- input_message_content=None,
- parse_mode=None,
- **kwargs):
+ __slots__ = (
+ 'reply_markup',
+ 'caption_entities',
+ 'voice_duration',
+ 'caption',
+ 'title',
+ 'voice_url',
+ 'parse_mode',
+ 'input_message_content',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ voice_url: str,
+ title: str,
+ voice_duration: int = None,
+ caption: str = None,
+ reply_markup: 'ReplyMarkup' = None,
+ input_message_content: 'InputMessageContent' = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
- super(InlineQueryResultVoice, self).__init__('voice', id)
+ super().__init__('voice', id)
self.voice_url = voice_url
self.title = title
# Optional
- if voice_duration:
- self.voice_duration = voice_duration
- if caption:
- self.caption = caption
- if parse_mode:
- self.parse_mode = parse_mode
- if reply_markup:
- self.reply_markup = reply_markup
- if input_message_content:
- self.input_message_content = input_message_content
+ self.voice_duration = voice_duration
+ self.caption = caption
+ self.parse_mode = parse_mode
+ self.caption_entities = caption_entities
+ self.reply_markup = reply_markup
+ self.input_message_content = input_message_content
diff --git a/telegramer/include/telegram/inline/inputcontactmessagecontent.py b/telegramer/include/telegram/inline/inputcontactmessagecontent.py
index c655b0f..fe7b9d7 100644
--- a/telegramer/include/telegram/inline/inputcontactmessagecontent.py
+++ b/telegramer/include/telegram/inline/inputcontactmessagecontent.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,18 +18,16 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InputContactMessageContent."""
+from typing import Any
+
from telegram import InputMessageContent
class InputContactMessageContent(InputMessageContent):
"""Represents the content of a contact message to be sent as the result of an inline query.
- Attributes:
- phone_number (:obj:`str`): Contact's phone number.
- first_name (:obj:`str`): Contact's first name.
- last_name (:obj:`str`): Optional. Contact's last name.
- vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard,
- 0-2048 bytes.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`phone_number` is equal.
Args:
phone_number (:obj:`str`): Contact's phone number.
@@ -39,12 +37,30 @@ class InputContactMessageContent(InputMessageContent):
0-2048 bytes.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ phone_number (:obj:`str`): Contact's phone number.
+ first_name (:obj:`str`): Contact's first name.
+ last_name (:obj:`str`): Optional. Contact's last name.
+ vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard,
+ 0-2048 bytes.
+
"""
- def __init__(self, phone_number, first_name, last_name=None, vcard=None, **kwargs):
+ __slots__ = ('vcard', 'first_name', 'last_name', 'phone_number', '_id_attrs')
+
+ def __init__(
+ self,
+ phone_number: str,
+ first_name: str,
+ last_name: str = None,
+ vcard: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.phone_number = phone_number
self.first_name = first_name
# Optionals
self.last_name = last_name
self.vcard = vcard
+
+ self._id_attrs = (self.phone_number,)
diff --git a/telegramer/include/telegram/inline/inputinvoicemessagecontent.py b/telegramer/include/telegram/inline/inputinvoicemessagecontent.py
new file mode 100644
index 0000000..622f0ed
--- /dev/null
+++ b/telegramer/include/telegram/inline/inputinvoicemessagecontent.py
@@ -0,0 +1,242 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains a class that represents a Telegram InputInvoiceMessageContent."""
+
+from typing import Any, List, Optional, TYPE_CHECKING
+
+from telegram import InputMessageContent, LabeledPrice
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+
+class InputInvoiceMessageContent(InputMessageContent):
+ """
+ Represents the content of a invoice message to be sent as the result of an inline query.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`title`, :attr:`description`, :attr:`payload`,
+ :attr:`provider_token`, :attr:`currency` and :attr:`prices` are equal.
+
+ .. versionadded:: 13.5
+
+ Args:
+ title (:obj:`str`): Product name, 1-32 characters
+ description (:obj:`str`): Product description, 1-255 characters
+ payload (:obj:`str`):Bot-defined invoice payload, 1-128 bytes. This will not be displayed
+ to the user, use for your internal processes.
+ provider_token (:obj:`str`): Payment provider token, obtained via
+ `@Botfather `_.
+ currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on
+ `currencies `_
+ prices (List[:class:`telegram.LabeledPrice`]): Price breakdown, a JSON-serialized list of
+ components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus,
+ etc.)
+ max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the smallest
+ units of the currency (integer, not float/double). For example, for a maximum tip of
+ US$ 1.45 pass ``max_tip_amount = 145``. See the ``exp`` parameter in
+ `currencies.json `_, it
+ shows the number of digits past the decimal point for each currency (2 for the majority
+ of currencies). Defaults to ``0``.
+ suggested_tip_amounts (List[:obj:`int`], optional): A JSON-serialized array of suggested
+ amounts of tip in the smallest units of the currency (integer, not float/double). At
+ most 4 suggested tip amounts can be specified. The suggested tip amounts must be
+ positive, passed in a strictly increased order and must not exceed
+ :attr:`max_tip_amount`.
+ provider_data (:obj:`str`, optional): A JSON-serialized object for data about the invoice,
+ which will be shared with the payment provider. A detailed description of the required
+ fields should be provided by the payment provider.
+ photo_url (:obj:`str`, optional): URL of the product photo for the invoice. Can be a photo
+ of the goods or a marketing image for a service. People like it better when they see
+ what they are paying for.
+ photo_size (:obj:`int`, optional): Photo size.
+ photo_width (:obj:`int`, optional): Photo width.
+ photo_height (:obj:`int`, optional): Photo height.
+ need_name (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's full name to
+ complete the order.
+ need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's
+ phone number to complete the order
+ need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email
+ address to complete the order.
+ need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's
+ shipping address to complete the order
+ send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's phone
+ number should be sent to provider.
+ send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email address
+ should be sent to provider.
+ is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on the
+ shipping method.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
+ Attributes:
+ title (:obj:`str`): Product name, 1-32 characters
+ description (:obj:`str`): Product description, 1-255 characters
+ payload (:obj:`str`):Bot-defined invoice payload, 1-128 bytes. This will not be displayed
+ to the user, use for your internal processes.
+ provider_token (:obj:`str`): Payment provider token, obtained via
+ `@Botfather `_.
+ currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on
+ `currencies `_
+ prices (List[:class:`telegram.LabeledPrice`]): Price breakdown, a JSON-serialized list of
+ components.
+ max_tip_amount (:obj:`int`): Optional. The maximum accepted amount for tips in the smallest
+ units of the currency (integer, not float/double).
+ suggested_tip_amounts (List[:obj:`int`]): Optional. A JSON-serialized array of suggested
+ amounts of tip in the smallest units of the currency (integer, not float/double).
+ provider_data (:obj:`str`): Optional. A JSON-serialized object for data about the invoice,
+ which will be shared with the payment provider.
+ photo_url (:obj:`str`): Optional. URL of the product photo for the invoice.
+ photo_size (:obj:`int`): Optional. Photo size.
+ photo_width (:obj:`int`): Optional. Photo width.
+ photo_height (:obj:`int`): Optional. Photo height.
+ need_name (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's full name to
+ complete the order.
+ need_phone_number (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's
+ phone number to complete the order
+ need_email (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's email
+ address to complete the order.
+ need_shipping_address (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's
+ shipping address to complete the order
+ send_phone_number_to_provider (:obj:`bool`): Optional. Pass :obj:`True`, if user's phone
+ number should be sent to provider.
+ send_email_to_provider (:obj:`bool`): Optional. Pass :obj:`True`, if user's email address
+ should be sent to provider.
+ is_flexible (:obj:`bool`): Optional. Pass :obj:`True`, if the final price depends on the
+ shipping method.
+
+ """
+
+ __slots__ = (
+ 'title',
+ 'description',
+ 'payload',
+ 'provider_token',
+ 'currency',
+ 'prices',
+ 'max_tip_amount',
+ 'suggested_tip_amounts',
+ 'provider_data',
+ 'photo_url',
+ 'photo_size',
+ 'photo_width',
+ 'photo_height',
+ 'need_name',
+ 'need_phone_number',
+ 'need_email',
+ 'need_shipping_address',
+ 'send_phone_number_to_provider',
+ 'send_email_to_provider',
+ 'is_flexible',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ title: str,
+ description: str,
+ payload: str,
+ provider_token: str,
+ currency: str,
+ prices: List[LabeledPrice],
+ max_tip_amount: int = None,
+ suggested_tip_amounts: List[int] = None,
+ provider_data: str = None,
+ photo_url: str = None,
+ photo_size: int = None,
+ photo_width: int = None,
+ photo_height: int = None,
+ need_name: bool = None,
+ need_phone_number: bool = None,
+ need_email: bool = None,
+ need_shipping_address: bool = None,
+ send_phone_number_to_provider: bool = None,
+ send_email_to_provider: bool = None,
+ is_flexible: bool = None,
+ **_kwargs: Any,
+ ):
+ # Required
+ self.title = title
+ self.description = description
+ self.payload = payload
+ self.provider_token = provider_token
+ self.currency = currency
+ self.prices = prices
+ # Optionals
+ self.max_tip_amount = int(max_tip_amount) if max_tip_amount else None
+ self.suggested_tip_amounts = (
+ [int(sta) for sta in suggested_tip_amounts] if suggested_tip_amounts else None
+ )
+ self.provider_data = provider_data
+ self.photo_url = photo_url
+ self.photo_size = int(photo_size) if photo_size else None
+ self.photo_width = int(photo_width) if photo_width else None
+ self.photo_height = int(photo_height) if photo_height else None
+ self.need_name = need_name
+ self.need_phone_number = need_phone_number
+ self.need_email = need_email
+ self.need_shipping_address = need_shipping_address
+ self.send_phone_number_to_provider = send_phone_number_to_provider
+ self.send_email_to_provider = send_email_to_provider
+ self.is_flexible = is_flexible
+
+ self._id_attrs = (
+ self.title,
+ self.description,
+ self.payload,
+ self.provider_token,
+ self.currency,
+ self.prices,
+ )
+
+ def __hash__(self) -> int:
+ # we override this as self.prices is a list and not hashable
+ prices = tuple(self.prices)
+ return hash(
+ (
+ self.title,
+ self.description,
+ self.payload,
+ self.provider_token,
+ self.currency,
+ prices,
+ )
+ )
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
+
+ data['prices'] = [price.to_dict() for price in self.prices]
+
+ return data
+
+ @classmethod
+ def de_json(
+ cls, data: Optional[JSONDict], bot: 'Bot'
+ ) -> Optional['InputInvoiceMessageContent']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ data['prices'] = LabeledPrice.de_list(data.get('prices'), bot)
+
+ return cls(**data, bot=bot)
diff --git a/telegramer/include/telegram/inline/inputlocationmessagecontent.py b/telegramer/include/telegram/inline/inputlocationmessagecontent.py
index 16010ec..22760bf 100644
--- a/telegramer/include/telegram/inline/inputlocationmessagecontent.py
+++ b/telegramer/include/telegram/inline/inputlocationmessagecontent.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,28 +18,71 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InputLocationMessageContent."""
+from typing import Any
+
from telegram import InputMessageContent
class InputLocationMessageContent(InputMessageContent):
+ # fmt: off
"""
Represents the content of a location message to be sent as the result of an inline query.
- Attributes:
- latitude (:obj:`float`): Latitude of the location in degrees.
- longitude (:obj:`float`): Longitude of the location in degrees.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`latitude` and :attr:`longitude` are equal.
Args:
latitude (:obj:`float`): Latitude of the location in degrees.
longitude (:obj:`float`): Longitude of the location in degrees.
+ horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location,
+ measured in meters; 0-1500.
live_period (:obj:`int`, optional): Period in seconds for which the location can be
updated, should be between 60 and 86400.
+ heading (:obj:`int`, optional): For live locations, a direction in which the user is
+ moving, in degrees. Must be between 1 and 360 if specified.
+ proximity_alert_radius (:obj:`int`, optional): For live locations, a maximum distance for
+ proximity alerts about approaching another chat member, in meters. Must be between 1
+ and 100000 if specified.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ latitude (:obj:`float`): Latitude of the location in degrees.
+ longitude (:obj:`float`): Longitude of the location in degrees.
+ horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location,
+ measured in meters.
+ live_period (:obj:`int`): Optional. Period in seconds for which the location can be
+ updated.
+ heading (:obj:`int`): Optional. For live locations, a direction in which the user is
+ moving, in degrees.
+ proximity_alert_radius (:obj:`int`): Optional. For live locations, a maximum distance for
+ proximity alerts about approaching another chat member, in meters.
+
"""
- def __init__(self, latitude, longitude, live_period=None, **kwargs):
+ __slots__ = ('longitude', 'horizontal_accuracy', 'proximity_alert_radius', 'live_period',
+ 'latitude', 'heading', '_id_attrs')
+ # fmt: on
+
+ def __init__(
+ self,
+ latitude: float,
+ longitude: float,
+ live_period: int = None,
+ horizontal_accuracy: float = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ **_kwargs: Any,
+ ):
# Required
self.latitude = latitude
self.longitude = longitude
- self.live_period = live_period
+
+ # Optionals
+ self.live_period = int(live_period) if live_period else None
+ self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None
+ self.heading = int(heading) if heading else None
+ self.proximity_alert_radius = (
+ int(proximity_alert_radius) if proximity_alert_radius else None
+ )
+
+ self._id_attrs = (self.latitude, self.longitude)
diff --git a/telegramer/include/telegram/inline/inputmessagecontent.py b/telegramer/include/telegram/inline/inputmessagecontent.py
index f7b7065..4362c72 100644
--- a/telegramer/include/telegram/inline/inputmessagecontent.py
+++ b/telegramer/include/telegram/inline/inputmessagecontent.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -25,8 +25,10 @@ class InputMessageContent(TelegramObject):
"""Base class for Telegram InputMessageContent Objects.
See: :class:`telegram.InputContactMessageContent`,
+ :class:`telegram.InputInvoiceMessageContent`,
:class:`telegram.InputLocationMessageContent`, :class:`telegram.InputTextMessageContent` and
:class:`telegram.InputVenueMessageContent` for more details.
"""
- pass
+
+ __slots__ = ()
diff --git a/telegramer/include/telegram/inline/inputtextmessagecontent.py b/telegramer/include/telegram/inline/inputtextmessagecontent.py
index 3e08d6b..c068aa7 100644
--- a/telegramer/include/telegram/inline/inputtextmessagecontent.py
+++ b/telegramer/include/telegram/inline/inputtextmessagecontent.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,34 +18,71 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InputTextMessageContent."""
-from telegram import InputMessageContent
+from typing import Any, Union, Tuple, List
+
+from telegram import InputMessageContent, MessageEntity
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
class InputTextMessageContent(InputMessageContent):
"""
Represents the content of a text message to be sent as the result of an inline query.
- Attributes:
- message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters.
- parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in your bot's message.
- disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in the
- sent message.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`message_text` is equal.
Args:
- message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters. Also found
- as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`.
+ message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters after entities
+ parsing. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
- bold, italic, fixed-width text or inline URLs in your bot's message.
+ bold, italic, fixed-width text or inline URLs in your bot's message. See the constants
+ in :class:`telegram.ParseMode` for the available modes.
+ entities (List[:class:`telegram.MessageEntity`], optional): List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in the
sent message.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters after entities
+ parsing.
+ parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
+ bold, italic, fixed-width text or inline URLs in your bot's message. See the constants
+ in :class:`telegram.ParseMode` for the available modes.
+ entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
+ entities that appear in the caption, which can be specified instead of
+ :attr:`parse_mode`.
+ disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in the
+ sent message.
+
"""
- def __init__(self, message_text, parse_mode=None, disable_web_page_preview=None, **kwargs):
+ __slots__ = ('disable_web_page_preview', 'parse_mode', 'entities', 'message_text', '_id_attrs')
+
+ def __init__(
+ self,
+ message_text: str,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
+ **_kwargs: Any,
+ ):
# Required
self.message_text = message_text
# Optionals
self.parse_mode = parse_mode
+ self.entities = entities
self.disable_web_page_preview = disable_web_page_preview
+
+ self._id_attrs = (self.message_text,)
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
+
+ if self.entities:
+ data['entities'] = [ce.to_dict() for ce in self.entities]
+
+ return data
diff --git a/telegramer/include/telegram/inline/inputvenuemessagecontent.py b/telegramer/include/telegram/inline/inputvenuemessagecontent.py
index d0f1fdc..c13107d 100644
--- a/telegramer/include/telegram/inline/inputvenuemessagecontent.py
+++ b/telegramer/include/telegram/inline/inputvenuemessagecontent.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,21 +18,21 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InputVenueMessageContent."""
+from typing import Any
+
from telegram import InputMessageContent
class InputVenueMessageContent(InputMessageContent):
"""Represents the content of a venue message to be sent as the result of an inline query.
- Attributes:
- latitude (:obj:`float`): Latitude of the location in degrees.
- longitude (:obj:`float`): Longitude of the location in degrees.
- title (:obj:`str`): Name of the venue.
- address (:obj:`str`): Address of the venue.
- foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue, if known.
- foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known.
- (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or
- "food/icecream".)
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`latitude`, :attr:`longitude` and :attr:`title`
+ are equal.
+
+ Note:
+ Foursquare details and Google Pace details are mutually exclusive. However, this
+ behaviour is undocumented and might be changed by Telegram.
Args:
latitude (:obj:`float`): Latitude of the location in degrees.
@@ -43,12 +43,47 @@ class InputVenueMessageContent(InputMessageContent):
foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known.
(For example, "arts_entertainment/default", "arts_entertainment/aquarium" or
"food/icecream".)
+ google_place_id (:obj:`str`, optional): Google Places identifier of the venue.
+ google_place_type (:obj:`str`, optional): Google Places type of the venue. (See
+ `supported types `_.)
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ latitude (:obj:`float`): Latitude of the location in degrees.
+ longitude (:obj:`float`): Longitude of the location in degrees.
+ title (:obj:`str`): Name of the venue.
+ address (:obj:`str`): Address of the venue.
+ foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue, if known.
+ foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known.
+ google_place_id (:obj:`str`): Optional. Google Places identifier of the venue.
+ google_place_type (:obj:`str`): Optional. Google Places type of the venue.
+
"""
- def __init__(self, latitude, longitude, title, address, foursquare_id=None,
- foursquare_type=None, **kwargs):
+ __slots__ = (
+ 'longitude',
+ 'google_place_type',
+ 'title',
+ 'address',
+ 'foursquare_id',
+ 'foursquare_type',
+ 'google_place_id',
+ 'latitude',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ latitude: float,
+ longitude: float,
+ title: str,
+ address: str,
+ foursquare_id: str = None,
+ foursquare_type: str = None,
+ google_place_id: str = None,
+ google_place_type: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.latitude = latitude
self.longitude = longitude
@@ -57,3 +92,11 @@ def __init__(self, latitude, longitude, title, address, foursquare_id=None,
# Optionals
self.foursquare_id = foursquare_id
self.foursquare_type = foursquare_type
+ self.google_place_id = google_place_id
+ self.google_place_type = google_place_type
+
+ self._id_attrs = (
+ self.latitude,
+ self.longitude,
+ self.title,
+ )
diff --git a/telegramer/include/telegram/keyboardbutton.py b/telegramer/include/telegram/keyboardbutton.py
index a8ed639..fe2bd87 100644
--- a/telegramer/include/telegram/keyboardbutton.py
+++ b/telegramer/include/telegram/keyboardbutton.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,9 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram KeyboardButton."""
-from telegram import TelegramObject
+from typing import Any
+
+from telegram import TelegramObject, KeyboardButtonPollType
class KeyboardButton(TelegramObject):
@@ -26,31 +28,56 @@ class KeyboardButton(TelegramObject):
This object represents one button of the reply keyboard. For simple text buttons String can be
used instead of this object to specify text of the button.
- Note:
- Optional fields are mutually exclusive.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`text`, :attr:`request_contact`, :attr:`request_location` and
+ :attr:`request_poll` are equal.
- Attributes:
- text (:obj:`str`): Text of the button.
- request_contact (:obj:`bool`): Optional. If the user's phone number will be sent.
- request_location (:obj:`bool`): Optional. If the user's current location will be sent.
+ Note:
+ * Optional fields are mutually exclusive.
+ * :attr:`request_contact` and :attr:`request_location` options will only work in Telegram
+ versions released after 9 April, 2016. Older clients will ignore them.
+ * :attr:`request_poll` option will only work in Telegram versions released after 23
+ January, 2020. Older clients will receive unsupported message.
Args:
text (:obj:`str`): Text of the button. If none of the optional fields are used, it will be
sent to the bot as a message when the button is pressed.
- request_contact (:obj:`bool`, optional): If True, the user's phone number will be sent as
- a contact when the button is pressed. Available in private chats only.
- request_location (:obj:`bool`, optional): If True, the user's current location will be sent
- when the button is pressed. Available in private chats only.
+ request_contact (:obj:`bool`, optional): If :obj:`True`, the user's phone number will be
+ sent as a contact when the button is pressed. Available in private chats only.
+ request_location (:obj:`bool`, optional): If :obj:`True`, the user's current location will
+ be sent when the button is pressed. Available in private chats only.
+ request_poll (:class:`KeyboardButtonPollType`, optional): If specified, the user will be
+ asked to create a poll and send it to the bot when the button is pressed. Available in
+ private chats only.
- Note:
- :attr:`request_contact` and :attr:`request_location` options will only work in Telegram
- versions released after 9 April, 2016. Older clients will ignore them.
+ Attributes:
+ text (:obj:`str`): Text of the button.
+ request_contact (:obj:`bool`): Optional. The user's phone number will be sent.
+ request_location (:obj:`bool`): Optional. The user's current location will be sent.
+ request_poll (:class:`KeyboardButtonPollType`): Optional. If the user should create a poll.
"""
- def __init__(self, text, request_contact=None, request_location=None, **kwargs):
+ __slots__ = ('request_location', 'request_contact', 'request_poll', 'text', '_id_attrs')
+
+ def __init__(
+ self,
+ text: str,
+ request_contact: bool = None,
+ request_location: bool = None,
+ request_poll: KeyboardButtonPollType = None,
+ **_kwargs: Any,
+ ):
# Required
self.text = text
# Optionals
self.request_contact = request_contact
self.request_location = request_location
+ self.request_poll = request_poll
+
+ self._id_attrs = (
+ self.text,
+ self.request_contact,
+ self.request_location,
+ self.request_poll,
+ )
diff --git a/telegramer/include/telegram/keyboardbuttonpolltype.py b/telegramer/include/telegram/keyboardbuttonpolltype.py
new file mode 100644
index 0000000..813ce97
--- /dev/null
+++ b/telegramer/include/telegram/keyboardbuttonpolltype.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+# pylint: disable=R0903
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a type of a Telegram Poll."""
+from typing import Any
+
+from telegram import TelegramObject
+
+
+class KeyboardButtonPollType(TelegramObject):
+ """This object represents type of a poll, which is allowed to be created
+ and sent when the corresponding button is pressed.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`type` is equal.
+
+ Attributes:
+ type (:obj:`str`): Optional. If :attr:`telegram.Poll.QUIZ` is passed, the user will be
+ allowed to create only polls in the quiz mode. If :attr:`telegram.Poll.REGULAR` is
+ passed, only regular polls will be allowed. Otherwise, the user will be allowed to
+ create a poll of any type.
+ """
+
+ __slots__ = ('type', '_id_attrs')
+
+ def __init__(self, type: str = None, **_kwargs: Any): # pylint: disable=W0622
+ self.type = type
+
+ self._id_attrs = (self.type,)
diff --git a/telegramer/include/telegram/loginurl.py b/telegramer/include/telegram/loginurl.py
new file mode 100644
index 0000000..7d6dc56
--- /dev/null
+++ b/telegramer/include/telegram/loginurl.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+# pylint: disable=R0903
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram LoginUrl."""
+from typing import Any
+
+from telegram import TelegramObject
+
+
+class LoginUrl(TelegramObject):
+ """This object represents a parameter of the inline keyboard button used to automatically
+ authorize a user. Serves as a great replacement for the Telegram Login Widget when the user is
+ coming from Telegram. All the user needs to do is tap/click a button and confirm that they want
+ to log in. Telegram apps support these buttons as of version 5.7.
+
+ Sample bot: `@discussbot `_
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`url` is equal.
+
+ Note:
+ You must always check the hash of the received data to verify the authentication
+ and the integrity of the data as described in
+ `Checking authorization `_
+
+ Args:
+ url (:obj:`str`): An HTTP URL to be opened with user authorization data added to the query
+ string when the button is pressed. If the user refuses to provide authorization data,
+ the original URL without information about the user will be opened. The data added is
+ the same as described in
+ `Receiving authorization data
+ `_
+ forward_text (:obj:`str`, optional): New text of the button in forwarded messages.
+ bot_username (:obj:`str`, optional): Username of a bot, which will be used for user
+ authorization. See
+ `Setting up a bot `_
+ for more details. If not specified, the current
+ bot's username will be assumed. The url's domain must be the same as the domain linked
+ with the bot. See
+ `Linking your domain to the bot
+ `_
+ for more details.
+ request_write_access (:obj:`bool`, optional): Pass :obj:`True` to request the permission
+ for your bot to send messages to the user.
+
+ Attributes:
+ url (:obj:`str`): An HTTP URL to be opened with user authorization data.
+ forward_text (:obj:`str`): Optional. New text of the button in forwarded messages.
+ bot_username (:obj:`str`): Optional. Username of a bot, which will be used for user
+ authorization.
+ request_write_access (:obj:`bool`): Optional. Pass :obj:`True` to request the permission
+ for your bot to send messages to the user.
+
+ """
+
+ __slots__ = ('bot_username', 'request_write_access', 'url', 'forward_text', '_id_attrs')
+
+ def __init__(
+ self,
+ url: str,
+ forward_text: bool = None,
+ bot_username: str = None,
+ request_write_access: bool = None,
+ **_kwargs: Any,
+ ):
+ # Required
+ self.url = url
+ # Optional
+ self.forward_text = forward_text
+ self.bot_username = bot_username
+ self.request_write_access = request_write_access
+
+ self._id_attrs = (self.url,)
diff --git a/telegramer/include/telegram/message.py b/telegramer/include/telegram/message.py
index 09b07d4..74d750f 100644
--- a/telegramer/include/telegram/message.py
+++ b/telegramer/include/telegram/message.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python
-# pylint: disable=R0902,R0912,R0913
+# pylint: disable=R0902,R0913
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,118 +18,118 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Message."""
+import datetime
import sys
from html import escape
-
-from telegram import (Animation, Audio, Contact, Document, Chat, Location, PhotoSize, Sticker,
- TelegramObject, User, Video, Voice, Venue, MessageEntity, Game, Invoice,
- SuccessfulPayment, VideoNote, PassportData)
-from telegram import ParseMode
-from telegram.utils.helpers import escape_markdown, to_timestamp, from_timestamp
-
-_UNDEFINED = object()
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, ClassVar, Tuple
+
+from telegram import (
+ Animation,
+ Audio,
+ Chat,
+ Contact,
+ Dice,
+ Document,
+ Game,
+ InlineKeyboardMarkup,
+ Invoice,
+ Location,
+ MessageEntity,
+ ParseMode,
+ PassportData,
+ PhotoSize,
+ Poll,
+ Sticker,
+ SuccessfulPayment,
+ TelegramObject,
+ User,
+ Venue,
+ Video,
+ VideoNote,
+ Voice,
+ VoiceChatStarted,
+ VoiceChatEnded,
+ VoiceChatParticipantsInvited,
+ ProximityAlertTriggered,
+ ReplyMarkup,
+ MessageAutoDeleteTimerChanged,
+ VoiceChatScheduled,
+)
+from telegram.utils.helpers import (
+ escape_markdown,
+ from_timestamp,
+ to_timestamp,
+ DEFAULT_NONE,
+ DEFAULT_20,
+)
+from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput
+
+if TYPE_CHECKING:
+ from telegram import (
+ Bot,
+ GameHighScore,
+ InputMedia,
+ MessageId,
+ InputMediaAudio,
+ InputMediaDocument,
+ InputMediaPhoto,
+ InputMediaVideo,
+ LabeledPrice,
+ )
class Message(TelegramObject):
+ # fmt: off
"""This object represents a message.
- Note:
- * In Python `from` is a reserved word, use `from_user` instead.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`message_id` and :attr:`chat` are equal.
- Attributes:
- message_id (:obj:`int`): Unique message identifier inside this chat.
- from_user (:class:`telegram.User`): Optional. Sender.
- date (:class:`datetime.datetime`): Date the message was sent.
- chat (:class:`telegram.Chat`): Conversation the message belongs to.
- forward_from (:class:`telegram.User`): Optional. Sender of the original message.
- forward_from_chat (:class:`telegram.Chat`): Optional. Information about the original
- channel.
- forward_from_message_id (:obj:`int`): Optional. Identifier of the original message in the
- channel.
- forward_date (:class:`datetime.datetime`): Optional. Date the original message was sent.
- reply_to_message (:class:`telegram.Message`): Optional. The original message.
- edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited.
- media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this
- message belongs to.
- text (:obj:`str`): Optional. The actual UTF-8 text of the message.
- entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like
- usernames, URLs, bot commands, etc. that appear in the text. See
- :attr:`Message.parse_entity` and :attr:`parse_entities` methods for how to use
- properly.
- caption_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like
- usernames, URLs, bot commands, etc. that appear in the caption. See
- :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` methods for how
- to use properly.
- audio (:class:`telegram.Audio`): Optional. Information about the file.
- document (:class:`telegram.Document`): Optional. Information about the file.
- animation (:class:`telegram.Animation`) Optional. Information about the file.
- For backward compatibility, when this field is set, the document field will also be
- set.
- game (:class:`telegram.Game`): Optional. Information about the game.
- photo (List[:class:`telegram.PhotoSize`]): Optional. Available sizes of the photo.
- sticker (:class:`telegram.Sticker`): Optional. Information about the sticker.
- video (:class:`telegram.Video`): Optional. Information about the video.
- voice (:class:`telegram.Voice`): Optional. Information about the file.
- video_note (:class:`telegram.VideoNote`): Optional. Information about the video message.
- new_chat_members (List[:class:`telegram.User`]): Optional. Information about new members to
- the chat. (the bot itself may be one of these members).
- caption (:obj:`str`): Optional. Caption for the document, photo or video, 0-200 characters.
- contact (:class:`telegram.Contact`): Optional. Information about the contact.
- location (:class:`telegram.Location`): Optional. Information about the location.
- venue (:class:`telegram.Venue`): Optional. Information about the venue.
- left_chat_member (:class:`telegram.User`): Optional. Information about the user that left
- the group. (this member may be the bot itself).
- new_chat_title (:obj:`str`): Optional. A chat title was changed to this value.
- new_chat_photo (List[:class:`telegram.PhotoSize`]): Optional. A chat photo was changed to
- this value.
- delete_chat_photo (:obj:`bool`): Optional. The chat photo was deleted.
- group_chat_created (:obj:`bool`): Optional. The group has been created.
- supergroup_chat_created (:obj:`bool`): Optional. The supergroup has been created.
- channel_chat_created (:obj:`bool`): Optional. The channel has been created.
- migrate_to_chat_id (:obj:`int`): Optional. The group has been migrated to a supergroup with
- the specified identifier.
- migrate_from_chat_id (:obj:`int`): Optional. The supergroup has been migrated from a group
- with the specified identifier.
- pinned_message (:class:`telegram.message`): Optional. Specified message was pinned.
- invoice (:class:`telegram.Invoice`): Optional. Information about the invoice.
- successful_payment (:class:`telegram.SuccessfulPayment`): Optional. Information about the
- payment.
- connected_website (:obj:`str`): Optional. The domain name of the website on which the user
- has logged in.
- forward_signature (:obj:`str`): Optional. Signature of the post author for messages
- forwarded from channels.
- author_signature (:obj:`str`): Optional. Signature of the post author for messages
- in channels.
- passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Note:
+ In Python ``from`` is a reserved word, use ``from_user`` instead.
Args:
message_id (:obj:`int`): Unique message identifier inside this chat.
- from_user (:class:`telegram.User`, optional): Sender, can be empty for messages sent
- to channels.
+ from_user (:class:`telegram.User`, optional): Sender of the message; empty for messages
+ sent to channels. For backward compatibility, this will contain a fake sender user in
+ non-channel chats, if the message was sent on behalf of a chat.
+ sender_chat (:class:`telegram.Chat`, optional): Sender of the message, sent on behalf of a
+ chat. For example, the channel itself for channel posts, the supergroup itself for
+ messages from anonymous group administrators, the linked channel for messages
+ automatically forwarded to the discussion group. For backward compatibility,
+ :attr:`from_user` contains a fake sender user in non-channel chats, if the message was
+ sent on behalf of a chat.
date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to
:class:`datetime.datetime`.
chat (:class:`telegram.Chat`): Conversation the message belongs to.
forward_from (:class:`telegram.User`, optional): For forwarded messages, sender of
the original message.
- forward_from_chat (:class:`telegram.Chat`, optional): For messages forwarded from a
- channel, information about the original channel.
+ forward_from_chat (:class:`telegram.Chat`, optional): For messages forwarded from channels
+ or from anonymous administrators, information about the original sender chat.
forward_from_message_id (:obj:`int`, optional): For forwarded channel posts, identifier of
the original message in the channel.
+ forward_sender_name (:obj:`str`, optional): Sender's name for messages forwarded from users
+ who disallow adding a link to their account in forwarded messages.
forward_date (:class:`datetime.datetime`, optional): For forwarded messages, date the
original message was sent in Unix time. Converted to :class:`datetime.datetime`.
+ is_automatic_forward (:obj:`bool`, optional): :obj:`True`, if the message is a channel post
+ that was automatically forwarded to the connected discussion group.
+
+ .. versionadded:: 13.9
reply_to_message (:class:`telegram.Message`, optional): For replies, the original message.
- Note that the Message object in this field will not contain further
- ``reply_to_message`` fields even if it itself is a reply.
edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix
time. Converted to :class:`datetime.datetime`.
+ has_protected_content (:obj:`bool`, optional): :obj:`True`, if the message can't be
+ forwarded.
+
+ .. versionadded:: 13.9
media_group_id (:obj:`str`, optional): The unique identifier of a media message group this
message belongs to.
text (str, optional): For text messages, the actual UTF-8 text of the message, 0-4096
characters. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`.
entities (List[:class:`telegram.MessageEntity`], optional): For text messages, special
entities like usernames, URLs, bot commands, etc. that appear in the text. See
- attr:`parse_entity` and attr:`parse_entities` methods for how to use properly.
+ :attr:`parse_entity` and :attr:`parse_entities` methods for how to use properly.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. For Messages with a
Caption. Special entities like usernames, URLs, bot commands, etc. that appear in the
caption. See :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities`
@@ -154,16 +154,19 @@ class Message(TelegramObject):
new_chat_members (List[:class:`telegram.User`], optional): New members that were added to
the group or supergroup and information about them (the bot itself may be one of these
members).
- caption (:obj:`str`, optional): Caption for the document, photo or video, 0-200 characters.
+ caption (:obj:`str`, optional): Caption for the animation, audio, document, photo, video
+ or voice, 0-1024 characters.
contact (:class:`telegram.Contact`, optional): Message is a shared contact, information
about the contact.
location (:class:`telegram.Location`, optional): Message is a shared location, information
about the location.
venue (:class:`telegram.Venue`, optional): Message is a venue, information about the venue.
+ For backward compatibility, when this field is set, the location field will also be
+ set.
left_chat_member (:class:`telegram.User`, optional): A member was removed from the group,
information about them (this member may be the bot itself).
new_chat_title (:obj:`str`, optional): A chat title was changed to this value.
- new_chat_photo (List[:class:`telegram.PhotoSize`], optional): A chat photo was change to
+ new_chat_photo (List[:class:`telegram.PhotoSize`], optional): A chat photo was changed to
this value.
delete_chat_photo (:obj:`bool`, optional): Service message: The chat photo was deleted.
group_chat_created (:obj:`bool`, optional): Service message: The group has been created.
@@ -175,7 +178,11 @@ class Message(TelegramObject):
channel_chat_created (:obj:`bool`, optional): Service message: The channel has been
created. This field can't be received in a message coming through updates, because bot
can't be a member of a channel when it is created. It can only be found in
- attr:`reply_to_message` if someone replies to a very first message in a channel.
+ :attr:`reply_to_message` if someone replies to a very first message in a channel.
+ message_auto_delete_timer_changed (:class:`telegram.MessageAutoDeleteTimerChanged`, \
+ optional): Service message: auto-delete timer settings changed in the chat.
+
+ .. versionadded:: 13.4
migrate_to_chat_id (:obj:`int`, optional): The group has been migrated to a supergroup with
the specified identifier. This number may be greater than 32 bits and some programming
languages may have difficulty/silent defects in interpreting it. But it is smaller than
@@ -186,8 +193,8 @@ class Message(TelegramObject):
programming languages may have difficulty/silent defects in interpreting it. But it is
smaller than 52 bits, so a signed 64 bit integer or double-precision float type are
safe for storing this identifier.
- pinned_message (:class:`telegram.message`, optional): Specified message was pinned. Note
- that the Message object in this field will not contain further attr:`reply_to_message`
+ pinned_message (:class:`telegram.Message`, optional): Specified message was pinned. Note
+ that the Message object in this field will not contain further :attr:`reply_to_message`
fields even if it is itself a reply.
invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment,
information about the invoice.
@@ -195,88 +202,343 @@ class Message(TelegramObject):
message about a successful payment, information about the payment.
connected_website (:obj:`str`, optional): The domain name of the website on which the user
has logged in.
- forward_signature (:obj:`str`, optional): Signature of the post author for messages
+ forward_signature (:obj:`str`, optional): For messages forwarded from channels, signature
+ of the post author if present.
+ author_signature (:obj:`str`, optional): Signature of the post author for messages in
+ channels, or the custom title of an anonymous group administrator.
+ passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data.
+ poll (:class:`telegram.Poll`, optional): Message is a native poll,
+ information about the poll.
+ dice (:class:`telegram.Dice`, optional): Message is a dice with random value from 1 to 6.
+ via_bot (:class:`telegram.User`, optional): Message was sent through an inline bot.
+ proximity_alert_triggered (:class:`telegram.ProximityAlertTriggered`, optional): Service
+ message. A user in the chat triggered another user's proximity alert while sharing
+ Live Location.
+ voice_chat_scheduled (:class:`telegram.VoiceChatScheduled`, optional): Service message:
+ voice chat scheduled.
+
+ .. versionadded:: 13.5
+ voice_chat_started (:class:`telegram.VoiceChatStarted`, optional): Service message: voice
+ chat started.
+
+ .. versionadded:: 13.4
+ voice_chat_ended (:class:`telegram.VoiceChatEnded`, optional): Service message: voice chat
+ ended.
+
+ .. versionadded:: 13.4
+ voice_chat_participants_invited (:class:`telegram.VoiceChatParticipantsInvited` optional):
+ Service message: new participants invited to a voice chat.
+
+ .. versionadded:: 13.4
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
+ to the message. ``login_url`` buttons are represented as ordinary url buttons.
+ bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
+
+ Attributes:
+ message_id (:obj:`int`): Unique message identifier inside this chat.
+ from_user (:class:`telegram.User`): Optional. Sender of the message; empty for messages
+ sent to channels. For backward compatibility, this will contain a fake sender user in
+ non-channel chats, if the message was sent on behalf of a chat.
+ sender_chat (:class:`telegram.Chat`): Optional. Sender of the message, sent on behalf of a
+ chat. For backward compatibility, :attr:`from_user` contains a fake sender user in
+ non-channel chats, if the message was sent on behalf of a chat.
+ date (:class:`datetime.datetime`): Date the message was sent.
+ chat (:class:`telegram.Chat`): Conversation the message belongs to.
+ forward_from (:class:`telegram.User`): Optional. Sender of the original message.
+ forward_from_chat (:class:`telegram.Chat`): Optional. For messages forwarded from channels
+ or from anonymous administrators, information about the original sender chat.
+ forward_from_message_id (:obj:`int`): Optional. Identifier of the original message in the
+ channel.
+ forward_date (:class:`datetime.datetime`): Optional. Date the original message was sent.
+ is_automatic_forward (:obj:`bool`): Optional. :obj:`True`, if the message is a channel post
+ that was automatically forwarded to the connected discussion group.
+
+ .. versionadded:: 13.9
+ reply_to_message (:class:`telegram.Message`): Optional. For replies, the original message.
+ Note that the Message object in this field will not contain further
+ ``reply_to_message`` fields even if it itself is a reply.
+ edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited.
+ has_protected_content (:obj:`bool`): Optional. :obj:`True`, if the message can't be
+ forwarded.
+
+ .. versionadded:: 13.9
+ media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this
+ message belongs to.
+ text (:obj:`str`): Optional. The actual UTF-8 text of the message.
+ entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like
+ usernames, URLs, bot commands, etc. that appear in the text. See
+ :attr:`Message.parse_entity` and :attr:`parse_entities` methods for how to use
+ properly.
+ caption_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like
+ usernames, URLs, bot commands, etc. that appear in the caption. See
+ :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` methods for how
+ to use properly.
+ audio (:class:`telegram.Audio`): Optional. Information about the file.
+ document (:class:`telegram.Document`): Optional. Information about the file.
+ animation (:class:`telegram.Animation`) Optional. Information about the file.
+ For backward compatibility, when this field is set, the document field will also be
+ set.
+ game (:class:`telegram.Game`): Optional. Information about the game.
+ photo (List[:class:`telegram.PhotoSize`]): Optional. Available sizes of the photo.
+ sticker (:class:`telegram.Sticker`): Optional. Information about the sticker.
+ video (:class:`telegram.Video`): Optional. Information about the video.
+ voice (:class:`telegram.Voice`): Optional. Information about the file.
+ video_note (:class:`telegram.VideoNote`): Optional. Information about the video message.
+ new_chat_members (List[:class:`telegram.User`]): Optional. Information about new members to
+ the chat. (the bot itself may be one of these members).
+ caption (:obj:`str`): Optional. Caption for the document, photo or video, 0-1024
+ characters.
+ contact (:class:`telegram.Contact`): Optional. Information about the contact.
+ location (:class:`telegram.Location`): Optional. Information about the location.
+ venue (:class:`telegram.Venue`): Optional. Information about the venue.
+ left_chat_member (:class:`telegram.User`): Optional. Information about the user that left
+ the group. (this member may be the bot itself).
+ new_chat_title (:obj:`str`): Optional. A chat title was changed to this value.
+ new_chat_photo (List[:class:`telegram.PhotoSize`]): Optional. A chat photo was changed to
+ this value.
+ delete_chat_photo (:obj:`bool`): Optional. The chat photo was deleted.
+ group_chat_created (:obj:`bool`): Optional. The group has been created.
+ supergroup_chat_created (:obj:`bool`): Optional. The supergroup has been created.
+ channel_chat_created (:obj:`bool`): Optional. The channel has been created.
+ message_auto_delete_timer_changed (:class:`telegram.MessageAutoDeleteTimerChanged`):
+ Optional. Service message: auto-delete timer settings changed in the chat.
+
+ .. versionadded:: 13.4
+ migrate_to_chat_id (:obj:`int`): Optional. The group has been migrated to a supergroup with
+ the specified identifier.
+ migrate_from_chat_id (:obj:`int`): Optional. The supergroup has been migrated from a group
+ with the specified identifier.
+ pinned_message (:class:`telegram.message`): Optional. Specified message was pinned.
+ invoice (:class:`telegram.Invoice`): Optional. Information about the invoice.
+ successful_payment (:class:`telegram.SuccessfulPayment`): Optional. Information about the
+ payment.
+ connected_website (:obj:`str`): Optional. The domain name of the website on which the user
+ has logged in.
+ forward_signature (:obj:`str`): Optional. Signature of the post author for messages
forwarded from channels.
- author_signature (:obj:`str`, optional): Signature of the post author for messages
- in channels.
- passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data
+ forward_sender_name (:obj:`str`): Optional. Sender's name for messages forwarded from users
+ who disallow adding a link to their account in forwarded messages.
+ author_signature (:obj:`str`): Optional. Signature of the post author for messages in
+ channels, or the custom title of an anonymous group administrator.
+ passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data.
+ poll (:class:`telegram.Poll`): Optional. Message is a native poll,
+ information about the poll.
+ dice (:class:`telegram.Dice`): Optional. Message is a dice.
+ via_bot (:class:`telegram.User`): Optional. Bot through which the message was sent.
+ proximity_alert_triggered (:class:`telegram.ProximityAlertTriggered`): Optional. Service
+ message. A user in the chat triggered another user's proximity alert while sharing
+ Live Location.
+ voice_chat_scheduled (:class:`telegram.VoiceChatScheduled`): Optional. Service message:
+ voice chat scheduled.
+
+ .. versionadded:: 13.5
+ voice_chat_started (:class:`telegram.VoiceChatStarted`): Optional. Service message: voice
+ chat started.
+
+ .. versionadded:: 13.4
+ voice_chat_ended (:class:`telegram.VoiceChatEnded`): Optional. Service message: voice chat
+ ended.
+
+ .. versionadded:: 13.4
+ voice_chat_participants_invited (:class:`telegram.VoiceChatParticipantsInvited`): Optional.
+ Service message: new participants invited to a voice chat.
+
+ .. versionadded:: 13.4
+ reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
+ to the message.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+
"""
- _effective_attachment = _UNDEFINED
-
- ATTACHMENT_TYPES = ['audio', 'game', 'animation', 'document', 'photo', 'sticker', 'video',
- 'voice', 'video_note', 'contact', 'location', 'venue', 'invoice',
- 'successful_payment']
- MESSAGE_TYPES = ['text', 'new_chat_members', 'new_chat_title', 'new_chat_photo',
- 'delete_chat_photo', 'group_chat_created', 'supergroup_chat_created',
- 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id',
- 'pinned_message', 'passport_data'] + ATTACHMENT_TYPES
-
- def __init__(self,
- message_id,
- from_user,
- date,
- chat,
- forward_from=None,
- forward_from_chat=None,
- forward_from_message_id=None,
- forward_date=None,
- reply_to_message=None,
- edit_date=None,
- text=None,
- entities=None,
- caption_entities=None,
- audio=None,
- document=None,
- game=None,
- photo=None,
- sticker=None,
- video=None,
- voice=None,
- video_note=None,
- new_chat_members=None,
- caption=None,
- contact=None,
- location=None,
- venue=None,
- left_chat_member=None,
- new_chat_title=None,
- new_chat_photo=None,
- delete_chat_photo=False,
- group_chat_created=False,
- supergroup_chat_created=False,
- channel_chat_created=False,
- migrate_to_chat_id=None,
- migrate_from_chat_id=None,
- pinned_message=None,
- invoice=None,
- successful_payment=None,
- forward_signature=None,
- author_signature=None,
- media_group_id=None,
- connected_website=None,
- animation=None,
- passport_data=None,
- bot=None,
- **kwargs):
+ # fmt: on
+ __slots__ = (
+ 'reply_markup',
+ 'audio',
+ 'contact',
+ 'migrate_to_chat_id',
+ 'forward_signature',
+ 'chat',
+ 'successful_payment',
+ 'game',
+ 'text',
+ 'forward_sender_name',
+ 'document',
+ 'new_chat_title',
+ 'forward_date',
+ 'group_chat_created',
+ 'media_group_id',
+ 'caption',
+ 'video',
+ 'bot',
+ 'entities',
+ 'via_bot',
+ 'new_chat_members',
+ 'connected_website',
+ 'animation',
+ 'migrate_from_chat_id',
+ 'forward_from',
+ 'sticker',
+ 'location',
+ 'venue',
+ 'edit_date',
+ 'reply_to_message',
+ 'passport_data',
+ 'pinned_message',
+ 'forward_from_chat',
+ 'new_chat_photo',
+ 'message_id',
+ 'delete_chat_photo',
+ 'from_user',
+ 'author_signature',
+ 'proximity_alert_triggered',
+ 'sender_chat',
+ 'dice',
+ 'forward_from_message_id',
+ 'caption_entities',
+ 'voice',
+ 'date',
+ 'supergroup_chat_created',
+ 'poll',
+ 'left_chat_member',
+ 'photo',
+ 'channel_chat_created',
+ 'invoice',
+ 'video_note',
+ '_effective_attachment',
+ 'message_auto_delete_timer_changed',
+ 'voice_chat_ended',
+ 'voice_chat_participants_invited',
+ 'voice_chat_started',
+ 'voice_chat_scheduled',
+ 'is_automatic_forward',
+ 'has_protected_content',
+ '_id_attrs',
+ )
+
+ ATTACHMENT_TYPES: ClassVar[List[str]] = [
+ 'audio',
+ 'game',
+ 'animation',
+ 'document',
+ 'photo',
+ 'sticker',
+ 'video',
+ 'voice',
+ 'video_note',
+ 'contact',
+ 'location',
+ 'venue',
+ 'invoice',
+ 'successful_payment',
+ ]
+ MESSAGE_TYPES: ClassVar[List[str]] = [
+ 'text',
+ 'new_chat_members',
+ 'left_chat_member',
+ 'new_chat_title',
+ 'new_chat_photo',
+ 'delete_chat_photo',
+ 'group_chat_created',
+ 'supergroup_chat_created',
+ 'channel_chat_created',
+ 'message_auto_delete_timer_changed',
+ 'migrate_to_chat_id',
+ 'migrate_from_chat_id',
+ 'pinned_message',
+ 'poll',
+ 'dice',
+ 'passport_data',
+ 'proximity_alert_triggered',
+ 'voice_chat_scheduled',
+ 'voice_chat_started',
+ 'voice_chat_ended',
+ 'voice_chat_participants_invited',
+ ] + ATTACHMENT_TYPES
+
+ def __init__(
+ self,
+ message_id: int,
+ date: datetime.datetime,
+ chat: Chat,
+ from_user: User = None,
+ forward_from: User = None,
+ forward_from_chat: Chat = None,
+ forward_from_message_id: int = None,
+ forward_date: datetime.datetime = None,
+ reply_to_message: 'Message' = None,
+ edit_date: datetime.datetime = None,
+ text: str = None,
+ entities: List['MessageEntity'] = None,
+ caption_entities: List['MessageEntity'] = None,
+ audio: Audio = None,
+ document: Document = None,
+ game: Game = None,
+ photo: List[PhotoSize] = None,
+ sticker: Sticker = None,
+ video: Video = None,
+ voice: Voice = None,
+ video_note: VideoNote = None,
+ new_chat_members: List[User] = None,
+ caption: str = None,
+ contact: Contact = None,
+ location: Location = None,
+ venue: Venue = None,
+ left_chat_member: User = None,
+ new_chat_title: str = None,
+ new_chat_photo: List[PhotoSize] = None,
+ delete_chat_photo: bool = False,
+ group_chat_created: bool = False,
+ supergroup_chat_created: bool = False,
+ channel_chat_created: bool = False,
+ migrate_to_chat_id: int = None,
+ migrate_from_chat_id: int = None,
+ pinned_message: 'Message' = None,
+ invoice: Invoice = None,
+ successful_payment: SuccessfulPayment = None,
+ forward_signature: str = None,
+ author_signature: str = None,
+ media_group_id: str = None,
+ connected_website: str = None,
+ animation: Animation = None,
+ passport_data: PassportData = None,
+ poll: Poll = None,
+ forward_sender_name: str = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ bot: 'Bot' = None,
+ dice: Dice = None,
+ via_bot: User = None,
+ proximity_alert_triggered: ProximityAlertTriggered = None,
+ sender_chat: Chat = None,
+ voice_chat_started: VoiceChatStarted = None,
+ voice_chat_ended: VoiceChatEnded = None,
+ voice_chat_participants_invited: VoiceChatParticipantsInvited = None,
+ message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged = None,
+ voice_chat_scheduled: VoiceChatScheduled = None,
+ is_automatic_forward: bool = None,
+ has_protected_content: bool = None,
+ **_kwargs: Any,
+ ):
# Required
self.message_id = int(message_id)
+ # Optionals
self.from_user = from_user
+ self.sender_chat = sender_chat
self.date = date
self.chat = chat
- # Optionals
self.forward_from = forward_from
self.forward_from_chat = forward_from_chat
self.forward_date = forward_date
+ self.is_automatic_forward = is_automatic_forward
self.reply_to_message = reply_to_message
self.edit_date = edit_date
+ self.has_protected_content = has_protected_content
self.text = text
- self.entities = entities or list()
- self.caption_entities = caption_entities or list()
+ self.entities = entities or []
+ self.caption_entities = caption_entities or []
self.audio = audio
self.game = game
self.document = document
- self.photo = photo or list()
+ self.photo = photo or []
self.sticker = sticker
self.video = video
self.voice = voice
@@ -285,52 +547,72 @@ def __init__(self,
self.contact = contact
self.location = location
self.venue = venue
- self.new_chat_members = new_chat_members or list()
+ self.new_chat_members = new_chat_members or []
self.left_chat_member = left_chat_member
self.new_chat_title = new_chat_title
- self.new_chat_photo = new_chat_photo or list()
+ self.new_chat_photo = new_chat_photo or []
self.delete_chat_photo = bool(delete_chat_photo)
self.group_chat_created = bool(group_chat_created)
self.supergroup_chat_created = bool(supergroup_chat_created)
self.migrate_to_chat_id = migrate_to_chat_id
self.migrate_from_chat_id = migrate_from_chat_id
self.channel_chat_created = bool(channel_chat_created)
+ self.message_auto_delete_timer_changed = message_auto_delete_timer_changed
self.pinned_message = pinned_message
self.forward_from_message_id = forward_from_message_id
self.invoice = invoice
self.successful_payment = successful_payment
self.connected_website = connected_website
self.forward_signature = forward_signature
+ self.forward_sender_name = forward_sender_name
self.author_signature = author_signature
self.media_group_id = media_group_id
self.animation = animation
self.passport_data = passport_data
-
+ self.poll = poll
+ self.dice = dice
+ self.via_bot = via_bot
+ self.proximity_alert_triggered = proximity_alert_triggered
+ self.voice_chat_scheduled = voice_chat_scheduled
+ self.voice_chat_started = voice_chat_started
+ self.voice_chat_ended = voice_chat_ended
+ self.voice_chat_participants_invited = voice_chat_participants_invited
+ self.reply_markup = reply_markup
self.bot = bot
- self._id_attrs = (self.message_id,)
+ self._effective_attachment = DEFAULT_NONE
+
+ self._id_attrs = (self.message_id, self.chat)
@property
- def chat_id(self):
+ def chat_id(self) -> int:
""":obj:`int`: Shortcut for :attr:`telegram.Chat.id` for :attr:`chat`."""
return self.chat.id
@property
- def link(self):
- """:obj:`str`: Convenience property. If the chat of the message is a supergroup or a
- channel and has a :attr:`Chat.username`, returns a t.me link of the message."""
- if self.chat.type in (Chat.SUPERGROUP, Chat.CHANNEL) and self.chat.username:
- return "https://t.me/{}/{}".format(self.chat.username, self.message_id)
+ def link(self) -> Optional[str]:
+ """:obj:`str`: Convenience property. If the chat of the message is not
+ a private chat or normal group, returns a t.me link of the message.
+ """
+ if self.chat.type not in [Chat.PRIVATE, Chat.GROUP]:
+ if self.chat.username:
+ to_link = self.chat.username
+ else:
+ # Get rid of leading -100 for supergroups
+ to_link = f"c/{str(self.chat.id)[4:]}"
+ return f"https://t.me/{to_link}/{self.message_id}"
return None
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Message']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(Message, cls).de_json(data, bot)
-
data['from_user'] = User.de_json(data.get('from'), bot)
+ data['sender_chat'] = Chat.de_json(data.get('sender_chat'), bot)
data['date'] = from_timestamp(data['date'])
data['chat'] = Chat.de_json(data.get('chat'), bot)
data['entities'] = MessageEntity.de_list(data.get('entities'), bot)
@@ -355,15 +637,49 @@ def de_json(cls, data, bot):
data['new_chat_members'] = User.de_list(data.get('new_chat_members'), bot)
data['left_chat_member'] = User.de_json(data.get('left_chat_member'), bot)
data['new_chat_photo'] = PhotoSize.de_list(data.get('new_chat_photo'), bot)
+ data['message_auto_delete_timer_changed'] = MessageAutoDeleteTimerChanged.de_json(
+ data.get('message_auto_delete_timer_changed'), bot
+ )
data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot)
data['invoice'] = Invoice.de_json(data.get('invoice'), bot)
data['successful_payment'] = SuccessfulPayment.de_json(data.get('successful_payment'), bot)
data['passport_data'] = PassportData.de_json(data.get('passport_data'), bot)
-
+ data['poll'] = Poll.de_json(data.get('poll'), bot)
+ data['dice'] = Dice.de_json(data.get('dice'), bot)
+ data['via_bot'] = User.de_json(data.get('via_bot'), bot)
+ data['proximity_alert_triggered'] = ProximityAlertTriggered.de_json(
+ data.get('proximity_alert_triggered'), bot
+ )
+ data['reply_markup'] = InlineKeyboardMarkup.de_json(data.get('reply_markup'), bot)
+ data['voice_chat_scheduled'] = VoiceChatScheduled.de_json(
+ data.get('voice_chat_scheduled'), bot
+ )
+ data['voice_chat_started'] = VoiceChatStarted.de_json(data.get('voice_chat_started'), bot)
+ data['voice_chat_ended'] = VoiceChatEnded.de_json(data.get('voice_chat_ended'), bot)
+ data['voice_chat_participants_invited'] = VoiceChatParticipantsInvited.de_json(
+ data.get('voice_chat_participants_invited'), bot
+ )
return cls(bot=bot, **data)
@property
- def effective_attachment(self):
+ def effective_attachment(
+ self,
+ ) -> Union[
+ Contact,
+ Document,
+ Animation,
+ Game,
+ Invoice,
+ Location,
+ List[PhotoSize],
+ Sticker,
+ SuccessfulPayment,
+ Venue,
+ Video,
+ VideoNote,
+ Voice,
+ None,
+ ]:
"""
:class:`telegram.Audio`
or :class:`telegram.Contact`
@@ -379,11 +695,11 @@ def effective_attachment(self):
or :class:`telegram.Video`
or :class:`telegram.VideoNote`
or :class:`telegram.Voice`: The attachment that this message was sent with. May be
- ``None`` if no attachment was sent.
+ :obj:`None` if no attachment was sent.
"""
- if self._effective_attachment is not _UNDEFINED:
- return self._effective_attachment
+ if self._effective_attachment is not DEFAULT_NONE:
+ return self._effective_attachment # type: ignore
for i in Message.ATTACHMENT_TYPES:
if getattr(self, i, None):
@@ -392,16 +708,14 @@ def effective_attachment(self):
else:
self._effective_attachment = None
- return self._effective_attachment
+ return self._effective_attachment # type: ignore
- def __getitem__(self, item):
- if item in self.__dict__.keys():
- return self.__dict__[item]
- elif item == 'chat_id':
- return self.chat.id
+ def __getitem__(self, item: str) -> Any: # pylint: disable=R1710
+ return self.chat.id if item == 'chat_id' else super().__getitem__(item)
- def to_dict(self):
- data = super(Message, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
# Required
data['date'] = to_timestamp(self.date)
@@ -423,292 +737,1138 @@ def to_dict(self):
return data
- def _quote(self, kwargs):
+ def _quote(self, quote: Optional[bool], reply_to_message_id: Optional[int]) -> Optional[int]:
"""Modify kwargs for replying with or without quoting."""
- if 'reply_to_message_id' in kwargs:
- if 'quote' in kwargs:
- del kwargs['quote']
-
- elif 'quote' in kwargs:
- if kwargs['quote']:
- kwargs['reply_to_message_id'] = self.message_id
+ if reply_to_message_id is not None:
+ return reply_to_message_id
- del kwargs['quote']
+ if quote is not None:
+ if quote:
+ return self.message_id
else:
- if self.chat.type != Chat.PRIVATE:
- kwargs['reply_to_message_id'] = self.message_id
+ if self.bot.defaults:
+ default_quote = self.bot.defaults.quote
+ else:
+ default_quote = None
+ if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote:
+ return self.message_id
- def reply_text(self, *args, **kwargs):
+ return None
+
+ def reply_text(
+ self,
+ text: str,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_message(update.message.chat_id, *args, **kwargs)
+ bot.send_message(update.effective_message.chat_id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the message is sent as an actual
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual
reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
- parameter will be ignored. Default: ``True`` in group chats and ``False`` in
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
private chats.
- """
- self._quote(kwargs)
- return self.bot.send_message(self.chat_id, *args, **kwargs)
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
- def reply_markdown(self, *args, **kwargs):
+ """
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_message(
+ chat_id=self.chat_id,
+ text=text,
+ parse_mode=parse_mode,
+ disable_web_page_preview=disable_web_page_preview,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ entities=entities,
+ protect_content=protect_content,
+ )
+
+ def reply_markdown(
+ self,
+ text: str,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_message(update.message.chat_id, parse_mode=ParseMode.MARKDOWN, *args,
- **kwargs)
+ bot.send_message(
+ update.effective_message.chat_id,
+ parse_mode=ParseMode.MARKDOWN,
+ *args,
+ **kwargs,
+ )
+
+ Sends a message with Markdown version 1 formatting.
- Sends a message with markdown formatting.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
+
+ Note:
+ :attr:`telegram.ParseMode.MARKDOWN` is a legacy mode, retained by Telegram for
+ backward compatibility. You should use :meth:`reply_markdown_v2` instead.
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the message is sent as an actual
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual
reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
- parameter will be ignored. Default: ``True`` in group chats and ``False`` in
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
private chats.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
"""
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_message(
+ chat_id=self.chat_id,
+ text=text,
+ parse_mode=ParseMode.MARKDOWN,
+ disable_web_page_preview=disable_web_page_preview,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ entities=entities,
+ protect_content=protect_content,
+ )
+
+ def reply_markdown_v2(
+ self,
+ text: str,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
- kwargs['parse_mode'] = ParseMode.MARKDOWN
+ bot.send_message(
+ update.effective_message.chat_id,
+ parse_mode=ParseMode.MARKDOWN_V2,
+ *args,
+ **kwargs,
+ )
- self._quote(kwargs)
+ Sends a message with markdown version 2 formatting.
- return self.bot.send_message(self.chat_id, *args, **kwargs)
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
- def reply_html(self, *args, **kwargs):
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
+ private chats.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+ """
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_message(
+ chat_id=self.chat_id,
+ text=text,
+ parse_mode=ParseMode.MARKDOWN_V2,
+ disable_web_page_preview=disable_web_page_preview,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ entities=entities,
+ protect_content=protect_content,
+ )
+
+ def reply_html(
+ self,
+ text: str,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_message(update.message.chat_id, parse_mode=ParseMode.HTML, *args, **kwargs)
+ bot.send_message(
+ update.effective_message.chat_id,
+ parse_mode=ParseMode.HTML,
+ *args,
+ **kwargs,
+ )
Sends a message with HTML formatting.
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the message is sent as an actual
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual
reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
- parameter will be ignored. Default: ``True`` in group chats and ``False`` in
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
private chats.
- """
-
- kwargs['parse_mode'] = ParseMode.HTML
- self._quote(kwargs)
-
- return self.bot.send_message(self.chat_id, *args, **kwargs)
-
- def reply_media_group(self, *args, **kwargs):
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+ """
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_message(
+ chat_id=self.chat_id,
+ text=text,
+ parse_mode=ParseMode.HTML,
+ disable_web_page_preview=disable_web_page_preview,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ entities=entities,
+ protect_content=protect_content,
+ )
+
+ def reply_media_group(
+ self,
+ media: List[
+ Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo']
+ ],
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> List['Message']:
"""Shortcut for::
- bot.reply_media_group(update.message.chat_id, *args, **kwargs)
+ bot.send_media_group(update.effective_message.chat_id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`.
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the media group is sent as an
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the media group is sent as an
actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``,
- this parameter will be ignored. Default: ``True`` in group chats and ``False`` in
- private chats.
+ this parameter will be ignored. Default: :obj:`True` in group chats and
+ :obj:`False` in private chats.
Returns:
List[:class:`telegram.Message`]: An array of the sent Messages.
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- self._quote(kwargs)
- return self.bot.send_media_group(self.chat_id, *args, **kwargs)
-
- def reply_photo(self, *args, **kwargs):
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_media_group(
+ chat_id=self.chat_id,
+ media=media,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def reply_photo(
+ self,
+ photo: Union[FileInput, 'PhotoSize'],
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_photo(update.message.chat_id, *args, **kwargs)
+ bot.send_photo(update.effective_message.chat_id, *args, **kwargs)
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the photo is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``,
+ this parameter will be ignored. Default: :obj:`True` in group chats and
+ :obj:`False` in private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_photo(self.chat_id, *args, **kwargs)
-
- def reply_audio(self, *args, **kwargs):
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_photo(
+ chat_id=self.chat_id,
+ photo=photo,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def reply_audio(
+ self,
+ audio: Union[FileInput, 'Audio'],
+ duration: int = None,
+ performer: str = None,
+ title: str = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_audio(update.message.chat_id, *args, **kwargs)
+ bot.send_audio(update.effective_message.chat_id, *args, **kwargs)
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the audio is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``,
+ this parameter will be ignored. Default: :obj:`True` in group chats and
+ :obj:`False` in private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_audio(self.chat_id, *args, **kwargs)
-
- def reply_document(self, *args, **kwargs):
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_audio(
+ chat_id=self.chat_id,
+ audio=audio,
+ duration=duration,
+ performer=performer,
+ title=title,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def reply_document(
+ self,
+ document: Union[FileInput, 'Document'],
+ filename: str = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ disable_content_type_detection: bool = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_document(update.message.chat_id, *args, **kwargs)
+ bot.send_document(update.effective_message.chat_id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`.
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the document is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
+ private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_document(self.chat_id, *args, **kwargs)
-
- def reply_animation(self, *args, **kwargs):
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_document(
+ chat_id=self.chat_id,
+ document=document,
+ filename=filename,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ disable_content_type_detection=disable_content_type_detection,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ protect_content=protect_content,
+ )
+
+ def reply_animation(
+ self,
+ animation: Union[FileInput, 'Animation'],
+ duration: int = None,
+ width: int = None,
+ height: int = None,
+ thumb: FileInput = None,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_animation(update.message.chat_id, *args, **kwargs)
+ bot.send_animation(update.effective_message.chat_id, *args, **kwargs)
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the animation is sent as an
+ actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``,
+ this parameter will be ignored. Default: :obj:`True` in group chats and
+ :obj:`False` in private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_animation(self.chat_id, *args, **kwargs)
-
- def reply_sticker(self, *args, **kwargs):
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_animation(
+ chat_id=self.chat_id,
+ animation=animation,
+ duration=duration,
+ width=width,
+ height=height,
+ thumb=thumb,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def reply_sticker(
+ self,
+ sticker: Union[FileInput, 'Sticker'],
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_sticker(update.message.chat_id, *args, **kwargs)
+ bot.send_sticker(update.effective_message.chat_id, *args, **kwargs)
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the sticker is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
+ private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_sticker(self.chat_id, *args, **kwargs)
-
- def reply_video(self, *args, **kwargs):
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_sticker(
+ chat_id=self.chat_id,
+ sticker=sticker,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def reply_video(
+ self,
+ video: Union[FileInput, 'Video'],
+ duration: int = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ width: int = None,
+ height: int = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ supports_streaming: bool = None,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_video(update.message.chat_id, *args, **kwargs)
+ bot.send_video(update.effective_message.chat_id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`.
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the video is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
+ private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_video(self.chat_id, *args, **kwargs)
-
- def reply_video_note(self, *args, **kwargs):
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_video(
+ chat_id=self.chat_id,
+ video=video,
+ duration=duration,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ width=width,
+ height=height,
+ parse_mode=parse_mode,
+ supports_streaming=supports_streaming,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def reply_video_note(
+ self,
+ video_note: Union[FileInput, 'VideoNote'],
+ duration: int = None,
+ length: int = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ filename: str = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_video_note(update.message.chat_id, *args, **kwargs)
+ bot.send_video_note(update.effective_message.chat_id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`.
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the video note is sent as an
+ actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``,
+ this parameter will be ignored. Default: :obj:`True` in group chats and
+ :obj:`False` in private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_video_note(self.chat_id, *args, **kwargs)
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_video_note(
+ chat_id=self.chat_id,
+ video_note=video_note,
+ duration=duration,
+ length=length,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def reply_voice(
+ self,
+ voice: Union[FileInput, 'Voice'],
+ duration: int = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_voice(update.effective_message.chat_id, *args, **kwargs)
- def reply_voice(self, *args, **kwargs):
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the voice note is sent as an
+ actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``,
+ this parameter will be ignored. Default: :obj:`True` in group chats and
+ :obj:`False` in private chats.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_voice(
+ chat_id=self.chat_id,
+ voice=voice,
+ duration=duration,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def reply_location(
+ self,
+ latitude: float = None,
+ longitude: float = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ location: Location = None,
+ live_period: int = None,
+ api_kwargs: JSONDict = None,
+ horizontal_accuracy: float = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_voice(update.message.chat_id, *args, **kwargs)
+ bot.send_location(update.effective_message.chat_id, *args, **kwargs)
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the location is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
+ private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_voice(self.chat_id, *args, **kwargs)
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_location(
+ chat_id=self.chat_id,
+ latitude=latitude,
+ longitude=longitude,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ location=location,
+ live_period=live_period,
+ api_kwargs=api_kwargs,
+ horizontal_accuracy=horizontal_accuracy,
+ heading=heading,
+ proximity_alert_radius=proximity_alert_radius,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def reply_venue(
+ self,
+ latitude: float = None,
+ longitude: float = None,
+ title: str = None,
+ address: str = None,
+ foursquare_id: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ venue: Venue = None,
+ foursquare_type: str = None,
+ api_kwargs: JSONDict = None,
+ google_place_id: str = None,
+ google_place_type: str = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_venue(update.effective_message.chat_id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the venue is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
+ private chats.
- def reply_location(self, *args, **kwargs):
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_venue(
+ chat_id=self.chat_id,
+ latitude=latitude,
+ longitude=longitude,
+ title=title,
+ address=address,
+ foursquare_id=foursquare_id,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ venue=venue,
+ foursquare_type=foursquare_type,
+ api_kwargs=api_kwargs,
+ google_place_id=google_place_id,
+ google_place_type=google_place_type,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def reply_contact(
+ self,
+ phone_number: str = None,
+ first_name: str = None,
+ last_name: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ contact: Contact = None,
+ vcard: str = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_location(update.message.chat_id, *args, **kwargs)
+ bot.send_contact(update.effective_message.chat_id, *args, **kwargs)
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the contact is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in
+ private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_location(self.chat_id, *args, **kwargs)
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_contact(
+ chat_id=self.chat_id,
+ phone_number=phone_number,
+ first_name=first_name,
+ last_name=last_name,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ contact=contact,
+ vcard=vcard,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def reply_poll(
+ self,
+ question: str,
+ options: List[str],
+ is_anonymous: bool = True,
+ type: str = Poll.REGULAR, # pylint: disable=W0622
+ allows_multiple_answers: bool = False,
+ correct_option_id: int = None,
+ is_closed: bool = None,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ explanation: str = None,
+ explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
+ open_period: int = None,
+ close_date: Union[int, datetime.datetime] = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_poll(update.effective_message.chat_id, *args, **kwargs)
- def reply_venue(self, *args, **kwargs):
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the poll is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``,
+ this parameter will be ignored. Default: :obj:`True` in group chats and
+ :obj:`False` in private chats.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_poll(
+ chat_id=self.chat_id,
+ question=question,
+ options=options,
+ is_anonymous=is_anonymous,
+ type=type,
+ allows_multiple_answers=allows_multiple_answers,
+ correct_option_id=correct_option_id,
+ is_closed=is_closed,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ explanation=explanation,
+ explanation_parse_mode=explanation_parse_mode,
+ open_period=open_period,
+ close_date=close_date,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ explanation_entities=explanation_entities,
+ protect_content=protect_content,
+ )
+
+ def reply_dice(
+ self,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ emoji: str = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_venue(update.message.chat_id, *args, **kwargs)
+ bot.send_dice(update.effective_message.chat_id, *args, **kwargs)
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the dice is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False`
+ in private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_venue(self.chat_id, *args, **kwargs)
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_dice(
+ chat_id=self.chat_id,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ emoji=emoji,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def reply_chat_action(
+ self,
+ action: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.send_chat_action(update.effective_message.chat_id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`.
- def reply_contact(self, *args, **kwargs):
+ .. versionadded:: 13.2
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.send_chat_action(
+ chat_id=self.chat_id,
+ action=action,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def reply_game(
+ self,
+ game_short_name: str,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_contact(update.message.chat_id, *args, **kwargs)
+ bot.send_game(update.effective_message.chat_id, *args, **kwargs)
- Keyword Args:
- quote (:obj:`bool`, optional): If set to ``True``, the photo is sent as an actual reply
- to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
- will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the game is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False`
+ in private chats.
+
+ .. versionadded:: 13.2
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- self._quote(kwargs)
- return self.bot.send_contact(self.chat_id, *args, **kwargs)
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_game(
+ chat_id=self.chat_id,
+ game_short_name=game_short_name,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def reply_invoice(
+ self,
+ title: str,
+ description: str,
+ payload: str,
+ provider_token: str,
+ currency: str,
+ prices: List['LabeledPrice'],
+ start_parameter: str = None,
+ photo_url: str = None,
+ photo_size: int = None,
+ photo_width: int = None,
+ photo_height: int = None,
+ need_name: bool = None,
+ need_phone_number: bool = None,
+ need_email: bool = None,
+ need_shipping_address: bool = None,
+ is_flexible: bool = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ provider_data: Union[str, object] = None,
+ send_phone_number_to_provider: bool = None,
+ send_email_to_provider: bool = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ quote: bool = None,
+ max_tip_amount: int = None,
+ suggested_tip_amounts: List[int] = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_invoice(update.effective_message.chat_id, *args, **kwargs)
- def forward(self, chat_id, disable_notification=False):
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`.
+
+ Warning:
+ As of API 5.2 :attr:`start_parameter` is an optional argument and therefore the order
+ of the arguments had to be changed. Use keyword arguments to make sure that the
+ arguments are passed correctly.
+
+ .. versionadded:: 13.2
+
+ .. versionchanged:: 13.5
+ As of Bot API 5.2, the parameter :attr:`start_parameter` is optional.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the invoice is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
+ parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False`
+ in private chats.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.send_invoice(
+ chat_id=self.chat_id,
+ title=title,
+ description=description,
+ payload=payload,
+ provider_token=provider_token,
+ currency=currency,
+ prices=prices,
+ start_parameter=start_parameter,
+ photo_url=photo_url,
+ photo_size=photo_size,
+ photo_width=photo_width,
+ photo_height=photo_height,
+ need_name=need_name,
+ need_phone_number=need_phone_number,
+ need_email=need_email,
+ need_shipping_address=need_shipping_address,
+ is_flexible=is_flexible,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ provider_data=provider_data,
+ send_phone_number_to_provider=send_phone_number_to_provider,
+ send_email_to_provider=send_email_to_provider,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ max_tip_amount=max_tip_amount,
+ suggested_tip_amounts=suggested_tip_amounts,
+ protect_content=protect_content,
+ )
+
+ def forward(
+ self,
+ chat_id: Union[int, str],
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
bot.forward_message(chat_id=chat_id,
- from_chat_id=update.message.chat_id,
- disable_notification=disable_notification,
- message_id=update.message.message_id)
+ from_chat_id=update.effective_message.chat_id,
+ message_id=update.effective_message.message_id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`.
+
+ Note:
+ Since the release of Bot API 5.5 it can be impossible to forward messages from
+ some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and
+ :attr:`telegram.Chat.has_protected_content` to check this.
+
+ As a workaround, it is still possible to use :meth:`copy`. However, this
+ behaviour is undocumented and might be changed by Telegram.
Returns:
:class:`telegram.Message`: On success, instance representing the message forwarded.
@@ -717,10 +1877,122 @@ def forward(self, chat_id, disable_notification=False):
return self.bot.forward_message(
chat_id=chat_id,
from_chat_id=self.chat_id,
+ message_id=self.message_id,
disable_notification=disable_notification,
- message_id=self.message_id)
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ def copy(
+ self,
+ chat_id: Union[int, str],
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> 'MessageId':
+ """Shortcut for::
+
+ bot.copy_message(chat_id=chat_id,
+ from_chat_id=update.effective_message.chat_id,
+ message_id=update.effective_message.message_id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`.
+
+ Returns:
+ :class:`telegram.MessageId`: On success, returns the MessageId of the sent message.
+
+ """
+ return self.bot.copy_message(
+ chat_id=chat_id,
+ from_chat_id=self.chat_id,
+ message_id=self.message_id,
+ caption=caption,
+ parse_mode=parse_mode,
+ caption_entities=caption_entities,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ allow_sending_without_reply=allow_sending_without_reply,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ def reply_copy(
+ self,
+ from_chat_id: Union[str, int],
+ message_id: int,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
+ reply_markup: ReplyMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ quote: bool = None,
+ protect_content: bool = None,
+ ) -> 'MessageId':
+ """Shortcut for::
- def edit_text(self, *args, **kwargs):
+ bot.copy_message(chat_id=message.chat.id,
+ from_chat_id=from_chat_id,
+ message_id=message_id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`.
+
+ Args:
+ quote (:obj:`bool`, optional): If set to :obj:`True`, the copy is sent as an actual
+ reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``,
+ this parameter will be ignored. Default: :obj:`True` in group chats and
+ :obj:`False` in private chats.
+
+ .. versionadded:: 13.1
+
+ Returns:
+ :class:`telegram.MessageId`: On success, returns the MessageId of the sent message.
+
+ """
+ reply_to_message_id = self._quote(quote, reply_to_message_id)
+ return self.bot.copy_message(
+ chat_id=self.chat_id,
+ from_chat_id=from_chat_id,
+ message_id=message_id,
+ caption=caption,
+ parse_mode=parse_mode,
+ caption_entities=caption_entities,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ allow_sending_without_reply=allow_sending_without_reply,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ def edit_text(
+ self,
+ text: str,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ ) -> Union['Message', bool]:
"""Shortcut for::
bot.edit_message_text(chat_id=message.chat_id,
@@ -728,19 +2000,40 @@ def edit_text(self, *args, **kwargs):
*args,
**kwargs)
+ For the documentation of the arguments, please see :meth:`telegram.Bot.edit_message_text`.
+
Note:
- You can only edit messages that the bot sent itself,
- therefore this method can only be used on the
- return value of the ``bot.send_*`` family of methods.
+ You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family
+ of methods) or channel posts, if the bot is an admin in that channel. However, this
+ behaviour is undocumented and might be changed by Telegram.
Returns:
- :class:`telegram.Message`: On success, instance representing the edited message.
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise ``True`` is returned.
"""
return self.bot.edit_message_text(
- chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs)
-
- def edit_caption(self, *args, **kwargs):
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ text=text,
+ parse_mode=parse_mode,
+ disable_web_page_preview=disable_web_page_preview,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ entities=entities,
+ inline_message_id=None,
+ )
+
+ def edit_caption(
+ self,
+ caption: str = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ ) -> Union['Message', bool]:
"""Shortcut for::
bot.edit_message_caption(chat_id=message.chat_id,
@@ -748,40 +2041,74 @@ def edit_caption(self, *args, **kwargs):
*args,
**kwargs)
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_message_caption`.
+
Note:
- You can only edit messages that the bot sent itself,
- therefore this method can only be used on the
- return value of the ``bot.send_*`` family of methods.
+ You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family
+ of methods) or channel posts, if the bot is an admin in that channel. However, this
+ behaviour is undocumented and might be changed by Telegram.
Returns:
- :class:`telegram.Message`: On success, instance representing the edited message.
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise ``True`` is returned.
"""
return self.bot.edit_message_caption(
- chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs)
-
- def edit_media(self, media, *args, **kwargs):
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ caption=caption,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ api_kwargs=api_kwargs,
+ caption_entities=caption_entities,
+ inline_message_id=None,
+ )
+
+ def edit_media(
+ self,
+ media: 'InputMedia' = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union['Message', bool]:
"""Shortcut for::
- bot.edit_message_media(chat_id=message.chat_id,
- message_id=message.message_id,
- *args,
- **kwargs)
+ bot.edit_message_media(chat_id=message.chat_id,
+ message_id=message.message_id,
+ *args,
+ **kwargs)
- Note:
- You can only edit messages that the bot sent itself,
- therefore this method can only be used on the
- return value of the ``bot.send_*`` family of methods.
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_message_media`.
- Returns:
- :class:`telegram.Message`: On success, instance representing the edited
- message.
+ Note:
+ You can only edit messages that the bot sent itself(i.e. of the ``bot.send_*`` family
+ of methods) or channel posts, if the bot is an admin in that channel. However, this
+ behaviour is undocumented and might be changed by Telegram.
- """
- return self.bot.edit_message_media(
- chat_id=self.chat_id, message_id=self.message_id, media=media, *args, **kwargs)
+ Returns:
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise ``True`` is returned.
- def edit_reply_markup(self, *args, **kwargs):
+ """
+ return self.bot.edit_message_media(
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ media=media,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ inline_message_id=None,
+ )
+
+ def edit_reply_markup(
+ self,
+ reply_markup: Optional['InlineKeyboardMarkup'] = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union['Message', bool]:
"""Shortcut for::
bot.edit_message_reply_markup(chat_id=message.chat_id,
@@ -789,18 +2116,184 @@ def edit_reply_markup(self, *args, **kwargs):
*args,
**kwargs)
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_message_reply_markup`.
+
Note:
- You can only edit messages that the bot sent itself,
- therefore this method can only be used on the
- return value of the ``bot.send_*`` family of methods.
+ You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family
+ of methods) or channel posts, if the bot is an admin in that channel. However, this
+ behaviour is undocumented and might be changed by Telegram.
Returns:
- :class:`telegram.Message`: On success, instance representing the edited message.
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise ``True`` is returned.
"""
return self.bot.edit_message_reply_markup(
- chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs)
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ inline_message_id=None,
+ )
+
+ def edit_live_location(
+ self,
+ latitude: float = None,
+ longitude: float = None,
+ location: Location = None,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ horizontal_accuracy: float = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ ) -> Union['Message', bool]:
+ """Shortcut for::
+
+ bot.edit_message_live_location(chat_id=message.chat_id,
+ message_id=message.message_id,
+ *args,
+ **kwargs)
- def delete(self, *args, **kwargs):
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_message_live_location`.
+
+ Note:
+ You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family
+ of methods) or channel posts, if the bot is an admin in that channel. However, this
+ behaviour is undocumented and might be changed by Telegram.
+
+ Returns:
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise :obj:`True` is returned.
+ """
+ return self.bot.edit_message_live_location(
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ latitude=latitude,
+ longitude=longitude,
+ location=location,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ horizontal_accuracy=horizontal_accuracy,
+ heading=heading,
+ proximity_alert_radius=proximity_alert_radius,
+ inline_message_id=None,
+ )
+
+ def stop_live_location(
+ self,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union['Message', bool]:
+ """Shortcut for::
+
+ bot.stop_message_live_location(chat_id=message.chat_id,
+ message_id=message.message_id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.stop_message_live_location`.
+
+ Note:
+ You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family
+ of methods) or channel posts, if the bot is an admin in that channel. However, this
+ behaviour is undocumented and might be changed by Telegram.
+
+ Returns:
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise :obj:`True` is returned.
+ """
+ return self.bot.stop_message_live_location(
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ inline_message_id=None,
+ )
+
+ def set_game_score(
+ self,
+ user_id: Union[int, str],
+ score: int,
+ force: bool = None,
+ disable_edit_message: bool = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Union['Message', bool]:
+ """Shortcut for::
+
+ bot.set_game_score(chat_id=message.chat_id,
+ message_id=message.message_id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.set_game_score`.
+
+ Note:
+ You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family
+ of methods) or channel posts, if the bot is an admin in that channel. However, this
+ behaviour is undocumented and might be changed by Telegram.
+
+ Returns:
+ :class:`telegram.Message`: On success, if edited message is sent by the bot, the
+ edited Message is returned, otherwise :obj:`True` is returned.
+ """
+ return self.bot.set_game_score(
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ user_id=user_id,
+ score=score,
+ force=force,
+ disable_edit_message=disable_edit_message,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ inline_message_id=None,
+ )
+
+ def get_game_high_scores(
+ self,
+ user_id: Union[int, str],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> List['GameHighScore']:
+ """Shortcut for::
+
+ bot.get_game_high_scores(chat_id=message.chat_id,
+ message_id=message.message_id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.get_game_high_scores`.
+
+ Note:
+ You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family
+ of methods) or channel posts, if the bot is an admin in that channel. However, this
+ behaviour is undocumented and might be changed by Telegram.
+
+ Returns:
+ List[:class:`telegram.GameHighScore`]
+ """
+ return self.bot.get_game_high_scores(
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ user_id=user_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ inline_message_id=None,
+ )
+
+ def delete(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Shortcut for::
bot.delete_message(chat_id=message.chat_id,
@@ -808,14 +2301,100 @@ def delete(self, *args, **kwargs):
*args,
**kwargs)
+ For the documentation of the arguments, please see :meth:`telegram.Bot.delete_message`.
+
Returns:
- :obj:`bool`: On success, ``True`` is returned.
+ :obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.delete_message(
- chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs)
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def stop_poll(
+ self,
+ reply_markup: InlineKeyboardMarkup = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Poll:
+ """Shortcut for::
+
+ bot.stop_poll(chat_id=message.chat_id,
+ message_id=message.message_id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.stop_poll`.
+
+ Returns:
+ :class:`telegram.Poll`: On success, the stopped Poll with the final results is
+ returned.
+
+ """
+ return self.bot.stop_poll(
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def pin(
+ self,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.pin_chat_message(chat_id=message.chat_id,
+ message_id=message.message_id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.pin_chat_message`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.pin_chat_message(
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ disable_notification=disable_notification,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def unpin(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.unpin_chat_message(chat_id=message.chat_id,
+ message_id=message.message_id,
+ *args,
+ **kwargs)
- def parse_entity(self, entity):
+ For the documentation of the arguments, please see :meth:`telegram.Bot.unpin_chat_message`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.unpin_chat_message(
+ chat_id=self.chat_id,
+ message_id=self.message_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def parse_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
Note:
@@ -825,22 +2404,27 @@ def parse_entity(self, entity):
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
- be an entity that belongs to this message.
+ be an entity that belongs to this message.
Returns:
- :obj:`str`: The text of the given entity
+ :obj:`str`: The text of the given entity.
+
+ Raises:
+ RuntimeError: If the message has no text.
"""
+ if not self.text:
+ raise RuntimeError("This Message has no 'text'.")
+
# Is it a narrow build, if so we don't need to convert
- if sys.maxunicode == 0xffff:
- return self.text[entity.offset:entity.offset + entity.length]
- else:
- entity_text = self.text.encode('utf-16-le')
- entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2]
+ if sys.maxunicode == 0xFFFF:
+ return self.text[entity.offset : entity.offset + entity.length]
+ entity_text = self.text.encode('utf-16-le')
+ entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode('utf-16-le')
- def parse_caption_entity(self, entity):
+ def parse_caption_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
Note:
@@ -850,22 +2434,27 @@ def parse_caption_entity(self, entity):
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
- be an entity that belongs to this message.
+ be an entity that belongs to this message.
Returns:
- :obj:`str`: The text of the given entity
+ :obj:`str`: The text of the given entity.
+
+ Raises:
+ RuntimeError: If the message has no caption.
"""
+ if not self.caption:
+ raise RuntimeError("This Message has no 'caption'.")
+
# Is it a narrow build, if so we don't need to convert
- if sys.maxunicode == 0xffff:
- return self.caption[entity.offset:entity.offset + entity.length]
- else:
- entity_text = self.caption.encode('utf-16-le')
- entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2]
+ if sys.maxunicode == 0xFFFF:
+ return self.caption[entity.offset : entity.offset + entity.length]
+ entity_text = self.caption.encode('utf-16-le')
+ entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode('utf-16-le')
- def parse_entities(self, types=None):
+ def parse_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this message filtered by their
@@ -893,10 +2482,11 @@ def parse_entities(self, types=None):
return {
entity: self.parse_entity(entity)
- for entity in self.entities if entity.type in types
+ for entity in (self.entities or [])
+ if entity.type in types
}
- def parse_caption_entities(self, types=None):
+ def parse_caption_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this message's caption filtered by their
@@ -924,59 +2514,126 @@ def parse_caption_entities(self, types=None):
return {
entity: self.parse_caption_entity(entity)
- for entity in self.caption_entities if entity.type in types
+ for entity in (self.caption_entities or [])
+ if entity.type in types
}
@staticmethod
- def _parse_html(message_text, entities, urled=False):
+ def _parse_html(
+ message_text: Optional[str],
+ entities: Dict[MessageEntity, str],
+ urled: bool = False,
+ offset: int = 0,
+ ) -> Optional[str]:
if message_text is None:
return None
- if not sys.maxunicode == 0xffff:
- message_text = message_text.encode('utf-16-le')
+ if sys.maxunicode != 0xFFFF:
+ message_text = message_text.encode('utf-16-le') # type: ignore
html_text = ''
last_offset = 0
- for entity, text in sorted(entities.items(), key=(lambda item: item[0].offset)):
- text = escape(text)
-
- if entity.type == MessageEntity.TEXT_LINK:
- insert = '{}'.format(entity.url, text)
- elif (entity.type == MessageEntity.URL) and urled:
- insert = '{0}'.format(text)
- elif entity.type == MessageEntity.BOLD:
- insert = '' + text + ''
- elif entity.type == MessageEntity.ITALIC:
- insert = '' + text + ''
- elif entity.type == MessageEntity.CODE:
- insert = '' + text + ''
- elif entity.type == MessageEntity.PRE:
- insert = '
' + text + '
'
+ sorted_entities = sorted(entities.items(), key=(lambda item: item[0].offset))
+ parsed_entities = []
+
+ for (entity, text) in sorted_entities:
+ if entity not in parsed_entities:
+ nested_entities = {
+ e: t
+ for (e, t) in sorted_entities
+ if e.offset >= entity.offset
+ and e.offset + e.length <= entity.offset + entity.length
+ and e != entity
+ }
+ parsed_entities.extend(list(nested_entities.keys()))
+
+ orig_text = text
+ text = escape(text)
+
+ if nested_entities:
+ text = Message._parse_html(
+ orig_text, nested_entities, urled=urled, offset=entity.offset
+ )
+
+ if entity.type == MessageEntity.TEXT_LINK:
+ insert = f'{text}'
+ elif entity.type == MessageEntity.TEXT_MENTION and entity.user:
+ insert = f'{text}'
+ elif entity.type == MessageEntity.URL and urled:
+ insert = f'{text}'
+ elif entity.type == MessageEntity.BOLD:
+ insert = '' + text + ''
+ elif entity.type == MessageEntity.ITALIC:
+ insert = '' + text + ''
+ elif entity.type == MessageEntity.CODE:
+ insert = '' + text + ''
+ elif entity.type == MessageEntity.PRE:
+ if entity.language:
+ insert = f'
{text}
'
+ else:
+ insert = '
' + text + '
'
+ elif entity.type == MessageEntity.UNDERLINE:
+ insert = '' + text + ''
+ elif entity.type == MessageEntity.STRIKETHROUGH:
+ insert = '' + text + ''
+ elif entity.type == MessageEntity.SPOILER:
+ insert = f'{text}'
+ else:
+ insert = text
+
+ if offset == 0:
+ if sys.maxunicode == 0xFFFF:
+ html_text += (
+ escape(message_text[last_offset : entity.offset - offset]) + insert
+ )
+ else:
+ html_text += (
+ escape(
+ message_text[ # type: ignore
+ last_offset * 2 : (entity.offset - offset) * 2
+ ].decode('utf-16-le')
+ )
+ + insert
+ )
+ else:
+ if sys.maxunicode == 0xFFFF:
+ html_text += message_text[last_offset : entity.offset - offset] + insert
+ else:
+ html_text += (
+ message_text[ # type: ignore
+ last_offset * 2 : (entity.offset - offset) * 2
+ ].decode('utf-16-le')
+ + insert
+ )
+
+ last_offset = entity.offset - offset + entity.length
+
+ if offset == 0:
+ if sys.maxunicode == 0xFFFF:
+ html_text += escape(message_text[last_offset:])
else:
- insert = text
-
- if sys.maxunicode == 0xffff:
- html_text += escape(message_text[last_offset:entity.offset]) + insert
+ html_text += escape(
+ message_text[last_offset * 2 :].decode('utf-16-le') # type: ignore
+ )
+ else:
+ if sys.maxunicode == 0xFFFF:
+ html_text += message_text[last_offset:]
else:
- html_text += escape(message_text[last_offset * 2:entity.offset * 2]
- .decode('utf-16-le')) + insert
-
- last_offset = entity.offset + entity.length
+ html_text += message_text[last_offset * 2 :].decode('utf-16-le') # type: ignore
- if sys.maxunicode == 0xffff:
- html_text += escape(message_text[last_offset:])
- else:
- html_text += escape(message_text[last_offset * 2:].decode('utf-16-le'))
return html_text
@property
- def text_html(self):
+ def text_html(self) -> str:
"""Creates an HTML-formatted string from the markup entities found in the message.
Use this if you want to retrieve the message text with the entities formatted as HTML in
the same way the original message was formatted.
+ .. versionchanged:: 13.10
+ Spoiler entities are now formatted as HTML.
+
Returns:
:obj:`str`: Message text with entities formatted as HTML.
@@ -984,12 +2641,15 @@ def text_html(self):
return self._parse_html(self.text, self.parse_entities(), urled=False)
@property
- def text_html_urled(self):
+ def text_html_urled(self) -> str:
"""Creates an HTML-formatted string from the markup entities found in the message.
Use this if you want to retrieve the message text with the entities formatted as HTML.
This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
+ .. versionchanged:: 13.10
+ Spoiler entities are now formatted as HTML.
+
Returns:
:obj:`str`: Message text with entities formatted as HTML.
@@ -997,125 +2657,354 @@ def text_html_urled(self):
return self._parse_html(self.text, self.parse_entities(), urled=True)
@property
- def caption_html(self):
+ def caption_html(self) -> str:
"""Creates an HTML-formatted string from the markup entities found in the message's
caption.
Use this if you want to retrieve the message caption with the caption entities formatted as
HTML in the same way the original message was formatted.
- Returns:
- :obj:`str`: Message caption with captionentities formatted as HTML.
+ .. versionchanged:: 13.10
+ Spoiler entities are now formatted as HTML.
+ Returns:
+ :obj:`str`: Message caption with caption entities formatted as HTML.
"""
return self._parse_html(self.caption, self.parse_caption_entities(), urled=False)
@property
- def caption_html_urled(self):
+ def caption_html_urled(self) -> str:
"""Creates an HTML-formatted string from the markup entities found in the message's
caption.
Use this if you want to retrieve the message caption with the caption entities formatted as
HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
+ .. versionchanged:: 13.10
+ Spoiler entities are now formatted as HTML.
+
Returns:
:obj:`str`: Message caption with caption entities formatted as HTML.
-
"""
return self._parse_html(self.caption, self.parse_caption_entities(), urled=True)
@staticmethod
- def _parse_markdown(message_text, entities, urled=False):
+ def _parse_markdown(
+ message_text: Optional[str],
+ entities: Dict[MessageEntity, str],
+ urled: bool = False,
+ version: int = 1,
+ offset: int = 0,
+ ) -> Optional[str]:
+ version = int(version)
+
if message_text is None:
return None
- if not sys.maxunicode == 0xffff:
- message_text = message_text.encode('utf-16-le')
+ if sys.maxunicode != 0xFFFF:
+ message_text = message_text.encode('utf-16-le') # type: ignore
markdown_text = ''
last_offset = 0
- for entity, text in sorted(entities.items(), key=(lambda item: item[0].offset)):
- text = escape_markdown(text)
-
- if entity.type == MessageEntity.TEXT_LINK:
- insert = '[{}]({})'.format(text, entity.url)
- elif (entity.type == MessageEntity.URL) and urled:
- insert = '[{0}]({0})'.format(text)
- elif entity.type == MessageEntity.BOLD:
- insert = '*' + text + '*'
- elif entity.type == MessageEntity.ITALIC:
- insert = '_' + text + '_'
- elif entity.type == MessageEntity.CODE:
- insert = '`' + text + '`'
- elif entity.type == MessageEntity.PRE:
- insert = '```' + text + '```'
+ sorted_entities = sorted(entities.items(), key=(lambda item: item[0].offset))
+ parsed_entities = []
+
+ for (entity, text) in sorted_entities:
+ if entity not in parsed_entities:
+ nested_entities = {
+ e: t
+ for (e, t) in sorted_entities
+ if e.offset >= entity.offset
+ and e.offset + e.length <= entity.offset + entity.length
+ and e != entity
+ }
+ parsed_entities.extend(list(nested_entities.keys()))
+
+ orig_text = text
+ text = escape_markdown(text, version=version)
+
+ if nested_entities:
+ if version < 2:
+ raise ValueError(
+ 'Nested entities are not supported for Markdown ' 'version 1'
+ )
+
+ text = Message._parse_markdown(
+ orig_text,
+ nested_entities,
+ urled=urled,
+ offset=entity.offset,
+ version=version,
+ )
+
+ if entity.type == MessageEntity.TEXT_LINK:
+ if version == 1:
+ url = entity.url
+ else:
+ # Links need special escaping. Also can't have entities nested within
+ url = escape_markdown(
+ entity.url, version=version, entity_type=MessageEntity.TEXT_LINK
+ )
+ insert = f'[{text}]({url})'
+ elif entity.type == MessageEntity.TEXT_MENTION and entity.user:
+ insert = f'[{text}](tg://user?id={entity.user.id})'
+ elif entity.type == MessageEntity.URL and urled:
+ if version == 1:
+ link = orig_text
+ else:
+ link = text
+ insert = f'[{link}]({orig_text})'
+ elif entity.type == MessageEntity.BOLD:
+ insert = '*' + text + '*'
+ elif entity.type == MessageEntity.ITALIC:
+ insert = '_' + text + '_'
+ elif entity.type == MessageEntity.CODE:
+ # Monospace needs special escaping. Also can't have entities nested within
+ insert = (
+ '`'
+ + escape_markdown(
+ orig_text, version=version, entity_type=MessageEntity.CODE
+ )
+ + '`'
+ )
+ elif entity.type == MessageEntity.PRE:
+ # Monospace needs special escaping. Also can't have entities nested within
+ code = escape_markdown(
+ orig_text, version=version, entity_type=MessageEntity.PRE
+ )
+ if entity.language:
+ prefix = '```' + entity.language + '\n'
+ else:
+ if code.startswith('\\'):
+ prefix = '```'
+ else:
+ prefix = '```\n'
+ insert = prefix + code + '```'
+ elif entity.type == MessageEntity.UNDERLINE:
+ if version == 1:
+ raise ValueError(
+ 'Underline entities are not supported for Markdown ' 'version 1'
+ )
+ insert = '__' + text + '__'
+ elif entity.type == MessageEntity.STRIKETHROUGH:
+ if version == 1:
+ raise ValueError(
+ 'Strikethrough entities are not supported for Markdown ' 'version 1'
+ )
+ insert = '~' + text + '~'
+ elif entity.type == MessageEntity.SPOILER:
+ if version == 1:
+ raise ValueError(
+ "Spoiler entities are not supported for Markdown version 1"
+ )
+ insert = f"||{text}||"
+ else:
+ insert = text
+
+ if offset == 0:
+ if sys.maxunicode == 0xFFFF:
+ markdown_text += (
+ escape_markdown(
+ message_text[last_offset : entity.offset - offset], version=version
+ )
+ + insert
+ )
+ else:
+ markdown_text += (
+ escape_markdown(
+ message_text[ # type: ignore
+ last_offset * 2 : (entity.offset - offset) * 2
+ ].decode('utf-16-le'),
+ version=version,
+ )
+ + insert
+ )
+ else:
+ if sys.maxunicode == 0xFFFF:
+ markdown_text += (
+ message_text[last_offset : entity.offset - offset] + insert
+ )
+ else:
+ markdown_text += (
+ message_text[ # type: ignore
+ last_offset * 2 : (entity.offset - offset) * 2
+ ].decode('utf-16-le')
+ + insert
+ )
+
+ last_offset = entity.offset - offset + entity.length
+
+ if offset == 0:
+ if sys.maxunicode == 0xFFFF:
+ markdown_text += escape_markdown(message_text[last_offset:], version=version)
else:
- insert = text
- if sys.maxunicode == 0xffff:
- markdown_text += escape_markdown(message_text[last_offset:entity.offset]) + insert
+ markdown_text += escape_markdown(
+ message_text[last_offset * 2 :].decode('utf-16-le'), # type: ignore
+ version=version,
+ )
+ else:
+ if sys.maxunicode == 0xFFFF:
+ markdown_text += message_text[last_offset:]
else:
- markdown_text += escape_markdown(message_text[last_offset * 2:entity.offset * 2]
- .decode('utf-16-le')) + insert
-
- last_offset = entity.offset + entity.length
+ markdown_text += message_text[last_offset * 2 :].decode( # type: ignore
+ 'utf-16-le'
+ )
- if sys.maxunicode == 0xffff:
- markdown_text += escape_markdown(message_text[last_offset:])
- else:
- markdown_text += escape_markdown(message_text[last_offset * 2:].decode('utf-16-le'))
return markdown_text
@property
- def text_markdown(self):
- """Creates an Markdown-formatted string from the markup entities found in the message.
+ def text_markdown(self) -> str:
+ """Creates an Markdown-formatted string from the markup entities found in the message
+ using :class:`telegram.ParseMode.MARKDOWN`.
Use this if you want to retrieve the message text with the entities formatted as Markdown
in the same way the original message was formatted.
+ Note:
+ :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for
+ backward compatibility. You should use :meth:`text_markdown_v2` instead.
+
Returns:
:obj:`str`: Message text with entities formatted as Markdown.
+ Raises:
+ :exc:`ValueError`: If the message contains underline, strikethrough, spoiler or nested
+ entities.
+
"""
return self._parse_markdown(self.text, self.parse_entities(), urled=False)
@property
- def text_markdown_urled(self):
- """Creates an Markdown-formatted string from the markup entities found in the message.
+ def text_markdown_v2(self) -> str:
+ """Creates an Markdown-formatted string from the markup entities found in the message
+ using :class:`telegram.ParseMode.MARKDOWN_V2`.
+
+ Use this if you want to retrieve the message text with the entities formatted as Markdown
+ in the same way the original message was formatted.
+
+ .. versionchanged:: 13.10
+ Spoiler entities are now formatted as Markdown V2.
+
+ Returns:
+ :obj:`str`: Message text with entities formatted as Markdown.
+ """
+ return self._parse_markdown(self.text, self.parse_entities(), urled=False, version=2)
+
+ @property
+ def text_markdown_urled(self) -> str:
+ """Creates an Markdown-formatted string from the markup entities found in the message
+ using :class:`telegram.ParseMode.MARKDOWN`.
Use this if you want to retrieve the message text with the entities formatted as Markdown.
This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
+ Note:
+ :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for
+ backward compatibility. You should use :meth:`text_markdown_v2_urled` instead.
+
Returns:
:obj:`str`: Message text with entities formatted as Markdown.
+ Raises:
+ :exc:`ValueError`: If the message contains underline, strikethrough, spoiler or nested
+ entities.
+
"""
return self._parse_markdown(self.text, self.parse_entities(), urled=True)
@property
- def caption_markdown(self):
+ def text_markdown_v2_urled(self) -> str:
+ """Creates an Markdown-formatted string from the markup entities found in the message
+ using :class:`telegram.ParseMode.MARKDOWN_V2`.
+
+ Use this if you want to retrieve the message text with the entities formatted as Markdown.
+ This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
+
+ .. versionchanged:: 13.10
+ Spoiler entities are now formatted as Markdown V2.
+
+ Returns:
+ :obj:`str`: Message text with entities formatted as Markdown.
+ """
+ return self._parse_markdown(self.text, self.parse_entities(), urled=True, version=2)
+
+ @property
+ def caption_markdown(self) -> str:
"""Creates an Markdown-formatted string from the markup entities found in the message's
- caption.
+ caption using :class:`telegram.ParseMode.MARKDOWN`.
Use this if you want to retrieve the message caption with the caption entities formatted as
Markdown in the same way the original message was formatted.
+ Note:
+ :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for
+ backward compatibility. You should use :meth:`caption_markdown_v2` instead.
+
Returns:
:obj:`str`: Message caption with caption entities formatted as Markdown.
+ Raises:
+ :exc:`ValueError`: If the message contains underline, strikethrough, spoiler or nested
+ entities.
+
"""
return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=False)
@property
- def caption_markdown_urled(self):
+ def caption_markdown_v2(self) -> str:
"""Creates an Markdown-formatted string from the markup entities found in the message's
- caption.
+ caption using :class:`telegram.ParseMode.MARKDOWN_V2`.
+
+ Use this if you want to retrieve the message caption with the caption entities formatted as
+ Markdown in the same way the original message was formatted.
+
+ .. versionchanged:: 13.10
+ Spoiler entities are now formatted as Markdown V2.
+
+ Returns:
+ :obj:`str`: Message caption with caption entities formatted as Markdown.
+ """
+ return self._parse_markdown(
+ self.caption, self.parse_caption_entities(), urled=False, version=2
+ )
+
+ @property
+ def caption_markdown_urled(self) -> str:
+ """Creates an Markdown-formatted string from the markup entities found in the message's
+ caption using :class:`telegram.ParseMode.MARKDOWN`.
Use this if you want to retrieve the message caption with the caption entities formatted as
Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
+ Note:
+ :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for
+ backward compatibility. You should use :meth:`caption_markdown_v2_urled` instead.
+
Returns:
:obj:`str`: Message caption with caption entities formatted as Markdown.
+ Raises:
+ :exc:`ValueError`: If the message contains underline, strikethrough, spoiler or nested
+ entities.
+
"""
return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=True)
+
+ @property
+ def caption_markdown_v2_urled(self) -> str:
+ """Creates an Markdown-formatted string from the markup entities found in the message's
+ caption using :class:`telegram.ParseMode.MARKDOWN_V2`.
+
+ Use this if you want to retrieve the message caption with the caption entities formatted as
+ Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
+
+ .. versionchanged:: 13.10
+ Spoiler entities are now formatted as Markdown V2.
+
+ Returns:
+ :obj:`str`: Message caption with caption entities formatted as Markdown.
+ """
+ return self._parse_markdown(
+ self.caption, self.parse_caption_entities(), urled=True, version=2
+ )
diff --git a/telegramer/include/telegram/messageautodeletetimerchanged.py b/telegramer/include/telegram/messageautodeletetimerchanged.py
new file mode 100644
index 0000000..21af1fb
--- /dev/null
+++ b/telegramer/include/telegram/messageautodeletetimerchanged.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a change in the Telegram message auto
+deletion.
+"""
+
+from typing import Any
+
+from telegram import TelegramObject
+
+
+class MessageAutoDeleteTimerChanged(TelegramObject):
+ """This object represents a service message about a change in auto-delete timer settings.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`message_auto_delete_time` is equal.
+
+ .. versionadded:: 13.4
+
+ Args:
+ message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the
+ chat.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
+ Attributes:
+ message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the
+ chat.
+
+ """
+
+ __slots__ = ('message_auto_delete_time', '_id_attrs')
+
+ def __init__(
+ self,
+ message_auto_delete_time: int,
+ **_kwargs: Any,
+ ):
+ self.message_auto_delete_time = int(message_auto_delete_time)
+
+ self._id_attrs = (self.message_auto_delete_time,)
diff --git a/telegramer/include/telegram/messageentity.py b/telegramer/include/telegram/messageentity.py
index 9617ee5..d4e1620 100644
--- a/telegramer/include/telegram/messageentity.py
+++ b/telegramer/include/telegram/messageentity.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,13 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram MessageEntity."""
-from telegram import User, TelegramObject
+from typing import TYPE_CHECKING, Any, List, Optional, ClassVar
+
+from telegram import TelegramObject, User, constants
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class MessageEntity(TelegramObject):
@@ -26,27 +32,46 @@ class MessageEntity(TelegramObject):
This object represents one special entity in a text message. For example, hashtags,
usernames, URLs, etc.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`type`, :attr:`offset` and :attr:`length` are equal.
+
+ Args:
+ type (:obj:`str`): Type of the entity. Currently, can be mention (@username), hashtag,
+ bot_command, url, email, phone_number, bold (bold text), italic (italic text),
+ strikethrough, spoiler (spoiler message), code (monowidth string), pre
+ (monowidth block), text_link (for clickable text URLs), text_mention
+ (for users without usernames).
+ offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity.
+ length (:obj:`int`): Length of the entity in UTF-16 code units.
+ url (:obj:`str`, optional): For :attr:`TEXT_LINK` only, url that will be opened after
+ user taps on the text.
+ user (:class:`telegram.User`, optional): For :attr:`TEXT_MENTION` only, the mentioned
+ user.
+ language (:obj:`str`, optional): For :attr:`PRE` only, the programming language of
+ the entity text.
+
Attributes:
type (:obj:`str`): Type of the entity.
offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity.
length (:obj:`int`): Length of the entity in UTF-16 code units.
url (:obj:`str`): Optional. Url that will be opened after user taps on the text.
user (:class:`telegram.User`): Optional. The mentioned user.
-
- Args:
- type (:obj:`str`): Type of the entity. Can be mention (@username), hashtag, bot_command,
- url, email, bold (bold text), italic (italic text), code (monowidth string), pre
- (monowidth block), text_link (for clickable text URLs), text_mention (for users
- without usernames).
- offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity.
- length (:obj:`int`): Length of the entity in UTF-16 code units.
- url (:obj:`str`, optional): For "text_link" only, url that will be opened after usertaps on
- the text.
- user (:class:`telegram.User`, optional): For "text_mention" only, the mentioned user.
+ language (:obj:`str`): Optional. Programming language of the entity text.
"""
- def __init__(self, type, offset, length, url=None, user=None, **kwargs):
+ __slots__ = ('length', 'url', 'user', 'type', 'language', 'offset', '_id_attrs')
+
+ def __init__(
+ self,
+ type: str, # pylint: disable=W0622
+ offset: int,
+ length: int,
+ url: str = None,
+ user: User = None,
+ language: str = None,
+ **_kwargs: Any,
+ ):
# Required
self.type = type
self.offset = offset
@@ -54,10 +79,14 @@ def __init__(self, type, offset, length, url=None, user=None, **kwargs):
# Optionals
self.url = url
self.user = user
+ self.language = language
+
+ self._id_attrs = (self.type, self.offset, self.length)
@classmethod
- def de_json(cls, data, bot):
- data = super(MessageEntity, cls).de_json(data, bot)
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['MessageEntity']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
if not data:
return None
@@ -66,45 +95,41 @@ def de_json(cls, data, bot):
return cls(**data)
- @classmethod
- def de_list(cls, data, bot):
- if not data:
- return list()
-
- entities = list()
- for entity in data:
- entities.append(cls.de_json(entity, bot))
-
- return entities
-
- MENTION = 'mention'
- """:obj:`str`: 'mention'"""
- HASHTAG = 'hashtag'
- """:obj:`str`: 'hashtag'"""
- CASHTAG = 'cashtag'
- """:obj:`str`: 'cashtag'"""
- PHONE_NUMBER = 'phone_number'
- """:obj:`str`: 'phone_number'"""
- BOT_COMMAND = 'bot_command'
- """:obj:`str`: 'bot_command'"""
- URL = 'url'
- """:obj:`str`: 'url'"""
- EMAIL = 'email'
- """:obj:`str`: 'email'"""
- BOLD = 'bold'
- """:obj:`str`: 'bold'"""
- ITALIC = 'italic'
- """:obj:`str`: 'italic'"""
- CODE = 'code'
- """:obj:`str`: 'code'"""
- PRE = 'pre'
- """:obj:`str`: 'pre'"""
- TEXT_LINK = 'text_link'
- """:obj:`str`: 'text_link'"""
- TEXT_MENTION = 'text_mention'
- """:obj:`str`: 'text_mention'"""
- ALL_TYPES = [
- MENTION, HASHTAG, CASHTAG, PHONE_NUMBER, BOT_COMMAND, URL,
- EMAIL, BOLD, ITALIC, CODE, PRE, TEXT_LINK, TEXT_MENTION
- ]
- """List[:obj:`str`]: List of all the types."""
+ MENTION: ClassVar[str] = constants.MESSAGEENTITY_MENTION
+ """:const:`telegram.constants.MESSAGEENTITY_MENTION`"""
+ HASHTAG: ClassVar[str] = constants.MESSAGEENTITY_HASHTAG
+ """:const:`telegram.constants.MESSAGEENTITY_HASHTAG`"""
+ CASHTAG: ClassVar[str] = constants.MESSAGEENTITY_CASHTAG
+ """:const:`telegram.constants.MESSAGEENTITY_CASHTAG`"""
+ PHONE_NUMBER: ClassVar[str] = constants.MESSAGEENTITY_PHONE_NUMBER
+ """:const:`telegram.constants.MESSAGEENTITY_PHONE_NUMBER`"""
+ BOT_COMMAND: ClassVar[str] = constants.MESSAGEENTITY_BOT_COMMAND
+ """:const:`telegram.constants.MESSAGEENTITY_BOT_COMMAND`"""
+ URL: ClassVar[str] = constants.MESSAGEENTITY_URL
+ """:const:`telegram.constants.MESSAGEENTITY_URL`"""
+ EMAIL: ClassVar[str] = constants.MESSAGEENTITY_EMAIL
+ """:const:`telegram.constants.MESSAGEENTITY_EMAIL`"""
+ BOLD: ClassVar[str] = constants.MESSAGEENTITY_BOLD
+ """:const:`telegram.constants.MESSAGEENTITY_BOLD`"""
+ ITALIC: ClassVar[str] = constants.MESSAGEENTITY_ITALIC
+ """:const:`telegram.constants.MESSAGEENTITY_ITALIC`"""
+ CODE: ClassVar[str] = constants.MESSAGEENTITY_CODE
+ """:const:`telegram.constants.MESSAGEENTITY_CODE`"""
+ PRE: ClassVar[str] = constants.MESSAGEENTITY_PRE
+ """:const:`telegram.constants.MESSAGEENTITY_PRE`"""
+ TEXT_LINK: ClassVar[str] = constants.MESSAGEENTITY_TEXT_LINK
+ """:const:`telegram.constants.MESSAGEENTITY_TEXT_LINK`"""
+ TEXT_MENTION: ClassVar[str] = constants.MESSAGEENTITY_TEXT_MENTION
+ """:const:`telegram.constants.MESSAGEENTITY_TEXT_MENTION`"""
+ UNDERLINE: ClassVar[str] = constants.MESSAGEENTITY_UNDERLINE
+ """:const:`telegram.constants.MESSAGEENTITY_UNDERLINE`"""
+ STRIKETHROUGH: ClassVar[str] = constants.MESSAGEENTITY_STRIKETHROUGH
+ """:const:`telegram.constants.MESSAGEENTITY_STRIKETHROUGH`"""
+ SPOILER: ClassVar[str] = constants.MESSAGEENTITY_SPOILER
+ """:const:`telegram.constants.MESSAGEENTITY_SPOILER`
+
+ .. versionadded:: 13.10
+ """
+ ALL_TYPES: ClassVar[List[str]] = constants.MESSAGEENTITY_ALL_TYPES
+ """:const:`telegram.constants.MESSAGEENTITY_ALL_TYPES`\n
+ List of all the types"""
diff --git a/telegramer/include/telegram/messageid.py b/telegramer/include/telegram/messageid.py
new file mode 100644
index 0000000..df7f47c
--- /dev/null
+++ b/telegramer/include/telegram/messageid.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents an instance of a Telegram MessageId."""
+from typing import Any
+
+from telegram import TelegramObject
+
+
+class MessageId(TelegramObject):
+ """This object represents a unique message identifier.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`message_id` is equal.
+
+ Attributes:
+ message_id (:obj:`int`): Unique message identifier
+ """
+
+ __slots__ = ('message_id', '_id_attrs')
+
+ def __init__(self, message_id: int, **_kwargs: Any):
+ self.message_id = int(message_id)
+
+ self._id_attrs = (self.message_id,)
diff --git a/telegramer/include/telegram/parsemode.py b/telegramer/include/telegram/parsemode.py
index e998d59..38cf051 100644
--- a/telegramer/include/telegram/parsemode.py
+++ b/telegramer/include/telegram/parsemode.py
@@ -2,7 +2,7 @@
# pylint: disable=R0903
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,12 +18,28 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Message Parse Modes."""
+from typing import ClassVar
+from telegram import constants
+from telegram.utils.deprecate import set_new_attribute_deprecated
-class ParseMode(object):
+
+class ParseMode:
"""This object represents a Telegram Message Parse Modes."""
- MARKDOWN = 'Markdown'
- """:obj:`str`: 'Markdown'"""
- HTML = 'HTML'
- """:obj:`str`: 'HTML'"""
+ __slots__ = ('__dict__',)
+
+ MARKDOWN: ClassVar[str] = constants.PARSEMODE_MARKDOWN
+ """:const:`telegram.constants.PARSEMODE_MARKDOWN`\n
+
+ Note:
+ :attr:`MARKDOWN` is a legacy mode, retained by Telegram for backward compatibility.
+ You should use :attr:`MARKDOWN_V2` instead.
+ """
+ MARKDOWN_V2: ClassVar[str] = constants.PARSEMODE_MARKDOWN_V2
+ """:const:`telegram.constants.PARSEMODE_MARKDOWN_V2`"""
+ HTML: ClassVar[str] = constants.PARSEMODE_HTML
+ """:const:`telegram.constants.PARSEMODE_HTML`"""
+
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
diff --git a/telegramer/include/telegram/passport/credentials.py b/telegramer/include/telegram/passport/credentials.py
index 638944d..d27bd31 100644
--- a/telegramer/include/telegram/passport/credentials.py
+++ b/telegramer/include/telegram/passport/credentials.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,33 +16,52 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=C0114, W0622
try:
import ujson as json
except ImportError:
- import json
+ import json # type: ignore[no-redef]
+
from base64 import b64decode
+from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, no_type_check
+
+try:
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP
+ from cryptography.hazmat.primitives.ciphers import Cipher
+ from cryptography.hazmat.primitives.ciphers.algorithms import AES
+ from cryptography.hazmat.primitives.ciphers.modes import CBC
+ from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512, Hash
+
+ CRYPTO_INSTALLED = True
+except ImportError:
+ default_backend = None
+ MGF1, OAEP, Cipher, AES, CBC = (None, None, None, None, None) # type: ignore[misc]
+ SHA1, SHA256, SHA512, Hash = (None, None, None, None) # type: ignore[misc]
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1
-from cryptography.hazmat.primitives.ciphers import Cipher
-from cryptography.hazmat.primitives.ciphers.algorithms import AES
-from cryptography.hazmat.primitives.ciphers.modes import CBC
-from cryptography.hazmat.primitives.hashes import SHA512, SHA256, Hash, SHA1
-from future.utils import bord
+ CRYPTO_INSTALLED = False
-from telegram import TelegramObject, TelegramError
+from telegram import TelegramError, TelegramObject
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class TelegramDecryptionError(TelegramError):
- """
- Something went wrong with decryption.
- """
+ """Something went wrong with decryption."""
- def __init__(self, message):
- super(TelegramDecryptionError, self).__init__("TelegramDecryptionError: "
- "{}".format(message))
+ __slots__ = ('_msg',)
+ def __init__(self, message: Union[str, Exception]):
+ super().__init__(f"TelegramDecryptionError: {message}")
+ self._msg = str(message)
+ def __reduce__(self) -> Tuple[type, Tuple[str]]:
+ return self.__class__, (self._msg,)
+
+
+@no_type_check
def decrypt(secret, hash, data):
"""
Decrypt per telegram docs at https://core.telegram.org/passport.
@@ -64,14 +83,19 @@ def decrypt(secret, hash, data):
:obj:`bytes`: The decrypted data as bytes.
"""
+ if not CRYPTO_INSTALLED:
+ raise RuntimeError(
+ 'To use Telegram Passports, PTB must be installed via `pip install '
+ 'python-telegram-bot[passport]`.'
+ )
# Make a SHA512 hash of secret + update
digest = Hash(SHA512(), backend=default_backend())
digest.update(secret + hash)
secret_hash_hash = digest.finalize()
# First 32 chars is our key, next 16 is the initialisation vector
- key, iv = secret_hash_hash[:32], secret_hash_hash[32:32 + 16]
+ key, init_vector = secret_hash_hash[:32], secret_hash_hash[32 : 32 + 16]
# Init a AES-CBC cipher and decrypt the data
- cipher = Cipher(AES(key), CBC(iv), backend=default_backend())
+ cipher = Cipher(AES(key), CBC(init_vector), backend=default_backend())
decryptor = cipher.decryptor()
data = decryptor.update(data) + decryptor.finalize()
# Calculate SHA256 hash of the decrypted data
@@ -81,11 +105,12 @@ def decrypt(secret, hash, data):
# If the newly calculated hash did not match the one telegram gave us
if data_hash != hash:
# Raise a error that is caught inside telegram.PassportData and transformed into a warning
- raise TelegramDecryptionError("Hashes are not equal! {} != {}".format(data_hash, hash))
+ raise TelegramDecryptionError(f"Hashes are not equal! {data_hash} != {hash}")
# Return data without padding
- return data[bord(data[0]):]
+ return data[data[0] :]
+@no_type_check
def decrypt_json(secret, hash, data):
"""Decrypts data using secret and hash and then decodes utf-8 string and loads json"""
return json.loads(decrypt(secret, hash, data).decode('utf-8'))
@@ -96,28 +121,41 @@ class EncryptedCredentials(TelegramObject):
Telegram Passport Documentation for a complete description of the data decryption and
authentication processes.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`data`, :attr:`hash` and :attr:`secret` are equal.
+
+ Note:
+ This object is decrypted only when originating from
+ :obj:`telegram.PassportData.decrypted_credentials`.
+
+ Args:
data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's
nonce, data hashes and secrets used for EncryptedPassportElement decryption and
authentication or base64 encrypted data.
hash (:obj:`str`): Base64-encoded data hash for data authentication.
secret (:obj:`str`): Decrypted or encrypted secret used for decryption.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's
nonce, data hashes and secrets used for EncryptedPassportElement decryption and
authentication or base64 encrypted data.
hash (:obj:`str`): Base64-encoded data hash for data authentication.
secret (:obj:`str`): Decrypted or encrypted secret used for decryption.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
-
- Note:
- This object is decrypted only when originating from
- :obj:`telegram.PassportData.decrypted_credentials`.
"""
- def __init__(self, data, hash, secret, bot=None, **kwargs):
+ __slots__ = (
+ 'hash',
+ 'secret',
+ 'bot',
+ 'data',
+ '_id_attrs',
+ '_decrypted_secret',
+ '_decrypted_data',
+ )
+
+ def __init__(self, data: str, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any):
# Required
self.data = data
self.hash = hash
@@ -127,19 +165,10 @@ def __init__(self, data, hash, secret, bot=None, **kwargs):
self.bot = bot
self._decrypted_secret = None
- self._decrypted_data = None
-
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- data = super(EncryptedCredentials, cls).de_json(data, bot)
-
- return cls(bot=bot, **data)
+ self._decrypted_data: Optional['Credentials'] = None
@property
- def decrypted_secret(self):
+ def decrypted_secret(self) -> str:
"""
:obj:`str`: Lazily decrypt and return secret.
@@ -148,6 +177,11 @@ def decrypted_secret(self):
private/public key but can also suggest malformed/tampered data.
"""
if self._decrypted_secret is None:
+ if not CRYPTO_INSTALLED:
+ raise RuntimeError(
+ 'To use Telegram Passports, PTB must be installed via `pip install '
+ 'python-telegram-bot[passport]`.'
+ )
# Try decrypting according to step 1 at
# https://core.telegram.org/passport#decrypting-data
# We make sure to base64 decode the secret first.
@@ -155,18 +189,17 @@ def decrypted_secret(self):
# is the default for OAEP, the algorithm is the default for PHP which is what
# Telegram's backend servers run.
try:
- self._decrypted_secret = self.bot.private_key.decrypt(b64decode(self.secret), OAEP(
- mgf=MGF1(algorithm=SHA1()),
- algorithm=SHA1(),
- label=None
- ))
- except ValueError as e:
+ self._decrypted_secret = self.bot.private_key.decrypt(
+ b64decode(self.secret),
+ OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None), # skipcq
+ )
+ except ValueError as exception:
# If decryption fails raise exception
- raise TelegramDecryptionError(e)
+ raise TelegramDecryptionError(exception) from exception
return self._decrypted_secret
@property
- def decrypted_data(self):
+ def decrypted_data(self) -> 'Credentials':
"""
:class:`telegram.Credentials`: Lazily decrypt and return credentials data. This object
also contains the user specified nonce as
@@ -177,10 +210,10 @@ def decrypted_data(self):
private/public key but can also suggest malformed/tampered data.
"""
if self._decrypted_data is None:
- self._decrypted_data = Credentials.de_json(decrypt_json(self.decrypted_secret,
- b64decode(self.hash),
- b64decode(self.data)),
- self.bot)
+ self._decrypted_data = Credentials.de_json(
+ decrypt_json(self.decrypted_secret, b64decode(self.hash), b64decode(self.data)),
+ self.bot,
+ )
return self._decrypted_data
@@ -191,7 +224,9 @@ class Credentials(TelegramObject):
nonce (:obj:`str`): Bot-specified nonce
"""
- def __init__(self, secure_data, nonce, bot=None, **kwargs):
+ __slots__ = ('bot', 'nonce', 'secure_data')
+
+ def __init__(self, secure_data: 'SecureData', nonce: str, bot: 'Bot' = None, **_kwargs: Any):
# Required
self.secure_data = secure_data
self.nonce = nonce
@@ -199,7 +234,10 @@ def __init__(self, secure_data, nonce, bot=None, **kwargs):
self.bot = bot
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Credentials']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
@@ -236,20 +274,37 @@ class SecureData(TelegramObject):
temporary registration.
"""
- def __init__(self,
- personal_details=None,
- passport=None,
- internal_passport=None,
- driver_license=None,
- identity_card=None,
- address=None,
- utility_bill=None,
- bank_statement=None,
- rental_agreement=None,
- passport_registration=None,
- temporary_registration=None,
- bot=None,
- **kwargs):
+ __slots__ = (
+ 'bot',
+ 'utility_bill',
+ 'personal_details',
+ 'temporary_registration',
+ 'address',
+ 'driver_license',
+ 'rental_agreement',
+ 'internal_passport',
+ 'identity_card',
+ 'bank_statement',
+ 'passport',
+ 'passport_registration',
+ )
+
+ def __init__(
+ self,
+ personal_details: 'SecureValue' = None,
+ passport: 'SecureValue' = None,
+ internal_passport: 'SecureValue' = None,
+ driver_license: 'SecureValue' = None,
+ identity_card: 'SecureValue' = None,
+ address: 'SecureValue' = None,
+ utility_bill: 'SecureValue' = None,
+ bank_statement: 'SecureValue' = None,
+ rental_agreement: 'SecureValue' = None,
+ passport_registration: 'SecureValue' = None,
+ temporary_registration: 'SecureValue' = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Optionals
self.temporary_registration = temporary_registration
self.passport_registration = passport_registration
@@ -266,14 +321,19 @@ def __init__(self,
self.bot = bot
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureData']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data['temporary_registration'] = SecureValue.de_json(data.get('temporary_registration'),
- bot=bot)
- data['passport_registration'] = SecureValue.de_json(data.get('passport_registration'),
- bot=bot)
+ data['temporary_registration'] = SecureValue.de_json(
+ data.get('temporary_registration'), bot=bot
+ )
+ data['passport_registration'] = SecureValue.de_json(
+ data.get('passport_registration'), bot=bot
+ )
data['rental_agreement'] = SecureValue.de_json(data.get('rental_agreement'), bot=bot)
data['bank_statement'] = SecureValue.de_json(data.get('bank_statement'), bot=bot)
data['utility_bill'] = SecureValue.de_json(data.get('utility_bill'), bot=bot)
@@ -314,15 +374,19 @@ class SecureValue(TelegramObject):
"""
- def __init__(self,
- data=None,
- front_side=None,
- reverse_side=None,
- selfie=None,
- files=None,
- translation=None,
- bot=None,
- **kwargs):
+ __slots__ = ('data', 'front_side', 'reverse_side', 'selfie', 'files', 'translation', 'bot')
+
+ def __init__(
+ self,
+ data: 'DataCredentials' = None,
+ front_side: 'FileCredentials' = None,
+ reverse_side: 'FileCredentials' = None,
+ selfie: 'FileCredentials' = None,
+ files: List['FileCredentials'] = None,
+ translation: List['FileCredentials'] = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
self.data = data
self.front_side = front_side
self.reverse_side = reverse_side
@@ -333,7 +397,10 @@ def __init__(self,
self.bot = bot
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureValue']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
@@ -346,8 +413,9 @@ def de_json(cls, data, bot):
return cls(bot=bot, **data)
- def to_dict(self):
- data = super(SecureValue, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
data['files'] = [p.to_dict() for p in self.files]
data['translation'] = [p.to_dict() for p in self.translation]
@@ -358,7 +426,9 @@ def to_dict(self):
class _CredentialsBase(TelegramObject):
"""Base class for DataCredentials and FileCredentials."""
- def __init__(self, hash, secret, bot=None, **kwargs):
+ __slots__ = ('hash', 'secret', 'file_hash', 'data_hash', 'bot')
+
+ def __init__(self, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any):
self.hash = hash
self.secret = secret
@@ -368,24 +438,6 @@ def __init__(self, hash, secret, bot=None, **kwargs):
self.bot = bot
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- return cls(bot=bot, **data)
-
- @classmethod
- def de_list(cls, data, bot):
- if not data:
- return []
-
- credentials = list()
- for c in data:
- credentials.append(cls.de_json(c, bot=bot))
-
- return credentials
-
class DataCredentials(_CredentialsBase):
"""
@@ -401,11 +453,14 @@ class DataCredentials(_CredentialsBase):
secret (:obj:`str`): Secret of encrypted data
"""
- def __init__(self, data_hash, secret, **kwargs):
- super(DataCredentials, self).__init__(data_hash, secret, **kwargs)
+ __slots__ = ()
- def to_dict(self):
- data = super(DataCredentials, self).to_dict()
+ def __init__(self, data_hash: str, secret: str, **_kwargs: Any):
+ super().__init__(data_hash, secret, **_kwargs)
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
del data['file_hash']
del data['hash']
@@ -415,23 +470,26 @@ def to_dict(self):
class FileCredentials(_CredentialsBase):
"""
- These credentials can be used to decrypt encrypted files from the front_side,
- reverse_side, selfie and files fields in EncryptedPassportData.
+ These credentials can be used to decrypt encrypted files from the front_side,
+ reverse_side, selfie and files fields in EncryptedPassportData.
- Args:
- file_hash (:obj:`str`): Checksum of encrypted file
- secret (:obj:`str`): Secret of encrypted file
+ Args:
+ file_hash (:obj:`str`): Checksum of encrypted file
+ secret (:obj:`str`): Secret of encrypted file
- Attributes:
- hash (:obj:`str`): Checksum of encrypted file
- secret (:obj:`str`): Secret of encrypted file
- """
+ Attributes:
+ hash (:obj:`str`): Checksum of encrypted file
+ secret (:obj:`str`): Secret of encrypted file
+ """
+
+ __slots__ = ()
- def __init__(self, file_hash, secret, **kwargs):
- super(FileCredentials, self).__init__(file_hash, secret, **kwargs)
+ def __init__(self, file_hash: str, secret: str, **_kwargs: Any):
+ super().__init__(file_hash, secret, **_kwargs)
- def to_dict(self):
- data = super(FileCredentials, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
del data['data_hash']
del data['hash']
diff --git a/telegramer/include/telegram/passport/data.py b/telegramer/include/telegram/passport/data.py
index 08658be..e1d38b5 100644
--- a/telegramer/include/telegram/passport/data.py
+++ b/telegramer/include/telegram/passport/data.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2017
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,8 +16,14 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=C0114
+from typing import TYPE_CHECKING, Any
+
from telegram import TelegramObject
+if TYPE_CHECKING:
+ from telegram import Bot
+
class PersonalDetails(TelegramObject):
"""
@@ -32,15 +38,43 @@ class PersonalDetails(TelegramObject):
country_code (:obj:`str`): Citizenship (ISO 3166-1 alpha-2 country code).
residence_country_code (:obj:`str`): Country of residence (ISO 3166-1 alpha-2 country
code).
- first_name (:obj:`str`): First Name in the language of the user's country of residence.
- middle_name (:obj:`str`): Optional. Middle Name in the language of the user's country of
+ first_name_native (:obj:`str`): First Name in the language of the user's country of
+ residence.
+ middle_name_native (:obj:`str`): Optional. Middle Name in the language of the user's
+ country of residence.
+ last_name_native (:obj:`str`): Last Name in the language of the user's country of
residence.
- last_name (:obj:`str`): Last Name in the language of the user's country of residence.
"""
- def __init__(self, first_name, last_name, birth_date, gender, country_code,
- residence_country_code, first_name_native, last_name_native, middle_name=None,
- middle_name_native=None, bot=None, **kwargs):
+ __slots__ = (
+ 'middle_name',
+ 'first_name_native',
+ 'last_name_native',
+ 'residence_country_code',
+ 'first_name',
+ 'last_name',
+ 'country_code',
+ 'gender',
+ 'bot',
+ 'middle_name_native',
+ 'birth_date',
+ )
+
+ def __init__(
+ self,
+ first_name: str,
+ last_name: str,
+ birth_date: str,
+ gender: str,
+ country_code: str,
+ residence_country_code: str,
+ first_name_native: str = None,
+ last_name_native: str = None,
+ middle_name: str = None,
+ middle_name_native: str = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
self.first_name = first_name
self.last_name = last_name
@@ -55,13 +89,6 @@ def __init__(self, first_name, last_name, birth_date, gender, country_code,
self.bot = bot
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- return cls(bot=bot, **data)
-
class ResidentialAddress(TelegramObject):
"""
@@ -76,8 +103,27 @@ class ResidentialAddress(TelegramObject):
post_code (:obj:`str`): Address post code.
"""
- def __init__(self, street_line1, street_line2, city, state, country_code,
- post_code, bot=None, **kwargs):
+ __slots__ = (
+ 'post_code',
+ 'city',
+ 'country_code',
+ 'street_line2',
+ 'street_line1',
+ 'bot',
+ 'state',
+ )
+
+ def __init__(
+ self,
+ street_line1: str,
+ street_line2: str,
+ city: str,
+ state: str,
+ country_code: str,
+ post_code: str,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
self.street_line1 = street_line1
self.street_line2 = street_line2
@@ -88,13 +134,6 @@ def __init__(self, street_line1, street_line2, city, state, country_code,
self.bot = bot
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- return cls(bot=bot, **data)
-
class IdDocumentData(TelegramObject):
"""
@@ -105,15 +144,10 @@ class IdDocumentData(TelegramObject):
expiry_date (:obj:`str`): Optional. Date of expiry, in DD.MM.YYYY format.
"""
- def __init__(self, document_no, expiry_date, bot=None, **kwargs):
+ __slots__ = ('document_no', 'bot', 'expiry_date')
+
+ def __init__(self, document_no: str, expiry_date: str, bot: 'Bot' = None, **_kwargs: Any):
self.document_no = document_no
self.expiry_date = expiry_date
self.bot = bot
-
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- return cls(bot=bot, **data)
diff --git a/telegramer/include/telegram/passport/encryptedpassportelement.py b/telegramer/include/telegram/passport/encryptedpassportelement.py
index db7332f..78b6972 100644
--- a/telegramer/include/telegram/passport/encryptedpassportelement.py
+++ b/telegramer/include/telegram/passport/encryptedpassportelement.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# flake8: noqa: E501
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,10 +18,20 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram EncryptedPassportElement."""
from base64 import b64decode
+from typing import TYPE_CHECKING, Any, List, Optional
-from telegram import (TelegramObject, PassportFile, PersonalDetails, IdDocumentData,
- ResidentialAddress)
+from telegram import (
+ IdDocumentData,
+ PassportFile,
+ PersonalDetails,
+ ResidentialAddress,
+ TelegramObject,
+)
from telegram.passport.credentials import decrypt_json
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot, Credentials
class EncryptedPassportElement(TelegramObject):
@@ -29,92 +39,116 @@ class EncryptedPassportElement(TelegramObject):
Contains information about documents or other Telegram Passport elements shared with the bot
by the user. The data has been automatically decrypted by python-telegram-bot.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`type`, :attr:`data`, :attr:`phone_number`, :attr:`email`,
+ :attr:`files`, :attr:`front_side`, :attr:`reverse_side` and :attr:`selfie` are equal.
+
+ Note:
+ This object is decrypted only when originating from
+ :obj:`telegram.PassportData.decrypted_data`.
+
+ Args:
type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license",
"identity_card", "internal_passport", "address", "utility_bill", "bank_statement",
"rental_agreement", "passport_registration", "temporary_registration", "phone_number",
"email".
- data (:class:`telegram.PersonalDetails` or :class:`telegram.IdDocument` or :class:`telegram.ResidentialAddress` or :obj:`str`):
- Optional. Decrypted or encrypted data, available for "personal_details", "passport",
+ data (:class:`telegram.PersonalDetails` | :class:`telegram.IdDocument` | \
+ :class:`telegram.ResidentialAddress` | :obj:`str`, optional):
+ Decrypted or encrypted data, available for "personal_details", "passport",
"driver_license", "identity_card", "identity_passport" and "address" types.
- phone_number (:obj:`str`): Optional. User's verified phone number, available only for
+ phone_number (:obj:`str`, optional): User's verified phone number, available only for
"phone_number" type.
- email (:obj:`str`): Optional. User's verified email address, available only for "email"
+ email (:obj:`str`, optional): User's verified email address, available only for "email"
type.
- files (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files
+ files (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files
with documents provided by the user, available for "utility_bill", "bank_statement",
"rental_agreement", "passport_registration" and "temporary_registration" types.
- front_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the
+ front_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
front side of the document, provided by the user. Available for "passport",
"driver_license", "identity_card" and "internal_passport".
- reverse_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the
+ reverse_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
reverse side of the document, provided by the user. Available for "driver_license" and
"identity_card".
- selfie (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the
+ selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
selfie of the user holding a document, provided by the user; available for "passport",
"driver_license", "identity_card" and "internal_passport".
- translation (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted
+ translation (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted
files with translated versions of documents provided by the user. Available if
requested for "passport", "driver_license", "identity_card", "internal_passport",
"utility_bill", "bank_statement", "rental_agreement", "passport_registration" and
"temporary_registration" types.
hash (:obj:`str`): Base64-encoded element hash for using in
:class:`telegram.PassportElementErrorUnspecified`.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license",
"identity_card", "internal_passport", "address", "utility_bill", "bank_statement",
"rental_agreement", "passport_registration", "temporary_registration", "phone_number",
"email".
- data (:class:`telegram.PersonalDetails` or :class:`telegram.IdDocument` or :class:`telegram.ResidentialAddress` or :obj:`str`, optional):
- Decrypted or encrypted data, available for "personal_details", "passport",
+ data (:class:`telegram.PersonalDetails` | :class:`telegram.IdDocument` | \
+ :class:`telegram.ResidentialAddress` | :obj:`str`):
+ Optional. Decrypted or encrypted data, available for "personal_details", "passport",
"driver_license", "identity_card", "identity_passport" and "address" types.
- phone_number (:obj:`str`, optional): User's verified phone number, available only for
+ phone_number (:obj:`str`): Optional. User's verified phone number, available only for
"phone_number" type.
- email (:obj:`str`, optional): User's verified email address, available only for "email"
+ email (:obj:`str`): Optional. User's verified email address, available only for "email"
type.
- files (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files
+ files (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files
with documents provided by the user, available for "utility_bill", "bank_statement",
"rental_agreement", "passport_registration" and "temporary_registration" types.
- front_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
+ front_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the
front side of the document, provided by the user. Available for "passport",
"driver_license", "identity_card" and "internal_passport".
- reverse_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
+ reverse_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the
reverse side of the document, provided by the user. Available for "driver_license" and
"identity_card".
- selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
+ selfie (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the
selfie of the user holding a document, provided by the user; available for "passport",
"driver_license", "identity_card" and "internal_passport".
- translation (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted
+ translation (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted
files with translated versions of documents provided by the user. Available if
requested for "passport", "driver_license", "identity_card", "internal_passport",
"utility_bill", "bank_statement", "rental_agreement", "passport_registration" and
"temporary_registration" types.
hash (:obj:`str`): Base64-encoded element hash for using in
:class:`telegram.PassportElementErrorUnspecified`.
- bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
- Note:
- This object is decrypted only when originating from
- :obj:`telegram.PassportData.decrypted_data`.
"""
- def __init__(self,
- type,
- data=None,
- phone_number=None,
- email=None,
- files=None,
- front_side=None,
- reverse_side=None,
- selfie=None,
- translation=None,
- hash=None,
- bot=None,
- credentials=None,
- **kwargs):
+ __slots__ = (
+ 'selfie',
+ 'files',
+ 'type',
+ 'translation',
+ 'email',
+ 'hash',
+ 'phone_number',
+ 'bot',
+ 'reverse_side',
+ 'front_side',
+ 'data',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ type: str, # pylint: disable=W0622
+ data: PersonalDetails = None,
+ phone_number: str = None,
+ email: str = None,
+ files: List[PassportFile] = None,
+ front_side: PassportFile = None,
+ reverse_side: PassportFile = None,
+ selfie: PassportFile = None,
+ translation: List[PassportFile] = None,
+ hash: str = None, # pylint: disable=W0622
+ bot: 'Bot' = None,
+ credentials: 'Credentials' = None, # pylint: disable=W0613
+ **_kwargs: Any,
+ ):
# Required
self.type = type
# Optionals
@@ -128,18 +162,27 @@ def __init__(self,
self.translation = translation
self.hash = hash
- self._id_attrs = (self.type, self.data, self.phone_number, self.email, self.files,
- self.front_side, self.reverse_side, self.selfie)
+ self._id_attrs = (
+ self.type,
+ self.data,
+ self.phone_number,
+ self.email,
+ self.files,
+ self.front_side,
+ self.reverse_side,
+ self.selfie,
+ )
self.bot = bot
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['EncryptedPassportElement']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(EncryptedPassportElement, cls).de_json(data, bot)
-
data['files'] = PassportFile.de_list(data.get('files'), bot) or None
data['front_side'] = PassportFile.de_json(data.get('front_side'), bot)
data['reverse_side'] = PassportFile.de_json(data.get('reverse_side'), bot)
@@ -149,55 +192,71 @@ def de_json(cls, data, bot):
return cls(bot=bot, **data)
@classmethod
- def de_json_decrypted(cls, data, bot, credentials):
+ def de_json_decrypted(
+ cls, data: Optional[JSONDict], bot: 'Bot', credentials: 'Credentials'
+ ) -> Optional['EncryptedPassportElement']:
+ """Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account
+ passport credentials.
+
+ Args:
+ data (Dict[:obj:`str`, ...]): The JSON data.
+ bot (:class:`telegram.Bot`): The bot associated with this object.
+ credentials (:class:`telegram.FileCredentials`): The credentials
+
+ Returns:
+ :class:`telegram.EncryptedPassportElement`:
+
+ """
if not data:
return None
- data = super(EncryptedPassportElement, cls).de_json(data, bot)
-
if data['type'] not in ('phone_number', 'email'):
secure_data = getattr(credentials.secure_data, data['type'])
if secure_data.data is not None:
# If not already decrypted
if not isinstance(data['data'], dict):
- data['data'] = decrypt_json(b64decode(secure_data.data.secret),
- b64decode(secure_data.data.hash),
- b64decode(data['data']))
+ data['data'] = decrypt_json(
+ b64decode(secure_data.data.secret),
+ b64decode(secure_data.data.hash),
+ b64decode(data['data']),
+ )
if data['type'] == 'personal_details':
data['data'] = PersonalDetails.de_json(data['data'], bot=bot)
- elif data['type'] in ('passport', 'internal_passport',
- 'driver_license', 'identity_card'):
+ elif data['type'] in (
+ 'passport',
+ 'internal_passport',
+ 'driver_license',
+ 'identity_card',
+ ):
data['data'] = IdDocumentData.de_json(data['data'], bot=bot)
elif data['type'] == 'address':
data['data'] = ResidentialAddress.de_json(data['data'], bot=bot)
- data['files'] = PassportFile.de_list_decrypted(data.get('files'), bot,
- secure_data.files) or None
- data['front_side'] = PassportFile.de_json_decrypted(data.get('front_side'), bot,
- secure_data.front_side)
- data['reverse_side'] = PassportFile.de_json_decrypted(data.get('reverse_side'), bot,
- secure_data.reverse_side)
- data['selfie'] = PassportFile.de_json_decrypted(data.get('selfie'), bot,
- secure_data.selfie)
- data['translation'] = PassportFile.de_list_decrypted(data.get('translation'), bot,
- secure_data.translation) or None
+ data['files'] = (
+ PassportFile.de_list_decrypted(data.get('files'), bot, secure_data.files) or None
+ )
+ data['front_side'] = PassportFile.de_json_decrypted(
+ data.get('front_side'), bot, secure_data.front_side
+ )
+ data['reverse_side'] = PassportFile.de_json_decrypted(
+ data.get('reverse_side'), bot, secure_data.reverse_side
+ )
+ data['selfie'] = PassportFile.de_json_decrypted(
+ data.get('selfie'), bot, secure_data.selfie
+ )
+ data['translation'] = (
+ PassportFile.de_list_decrypted(
+ data.get('translation'), bot, secure_data.translation
+ )
+ or None
+ )
return cls(bot=bot, **data)
- @classmethod
- def de_list(cls, data, bot):
- if not data:
- return []
-
- encrypted_passport_elements = list()
- for element in data:
- encrypted_passport_elements.append(cls.de_json(element, bot))
-
- return encrypted_passport_elements
-
- def to_dict(self):
- data = super(EncryptedPassportElement, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
if self.files:
data['files'] = [p.to_dict() for p in self.files]
diff --git a/telegramer/include/telegram/passport/passportdata.py b/telegramer/include/telegram/passport/passportdata.py
index 67101da..40f7e72 100644
--- a/telegramer/include/telegram/passport/passportdata.py
+++ b/telegramer/include/telegram/passport/passportdata.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,62 +18,78 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Contains information about Telegram Passport data shared with the bot by the user."""
+from typing import TYPE_CHECKING, Any, List, Optional
+
from telegram import EncryptedCredentials, EncryptedPassportElement, TelegramObject
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot, Credentials
class PassportData(TelegramObject):
"""Contains information about Telegram Passport data shared with the bot by the user.
- Attributes:
- data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information
- about documents and other Telegram Passport elements that was shared with the bot.
- credentials (:class:`telegram.EncryptedCredentials`): Encrypted credentials.
- bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
+ Note:
+ To be able to decrypt this object, you must pass your ``private_key`` to either
+ :class:`telegram.Updater` or :class:`telegram.Bot`. Decrypted data is then found in
+ :attr:`decrypted_data` and the payload can be found in :attr:`decrypted_credentials`'s
+ attribute :attr:`telegram.Credentials.payload`.
Args:
data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information
about documents and other Telegram Passport elements that was shared with the bot.
- credentials (:obj:`str`): Encrypted credentials.
+ credentials (:class:`telegram.EncryptedCredentials`)): Encrypted credentials.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Note:
- To be able to decrypt this object, you must pass your private_key to either
- :class:`telegram.Updater` or :class:`telegram.Bot`. Decrypted data is then found in
- :attr:`decrypted_data` and the payload can be found in :attr:`decrypted_credentials`'s
- attribute :attr:`telegram.Credentials.payload`.
+ Attributes:
+ data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information
+ about documents and other Telegram Passport elements that was shared with the bot.
+ credentials (:class:`telegram.EncryptedCredentials`): Encrypted credentials.
+ bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
"""
- def __init__(self, data, credentials, bot=None, **kwargs):
+ __slots__ = ('bot', 'credentials', 'data', '_decrypted_data', '_id_attrs')
+
+ def __init__(
+ self,
+ data: List[EncryptedPassportElement],
+ credentials: EncryptedCredentials,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
self.data = data
self.credentials = credentials
self.bot = bot
- self._decrypted_data = None
+ self._decrypted_data: Optional[List[EncryptedPassportElement]] = None
self._id_attrs = tuple([x.type for x in data] + [credentials.hash])
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['PassportData']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(PassportData, cls).de_json(data, bot)
-
data['data'] = EncryptedPassportElement.de_list(data.get('data'), bot)
data['credentials'] = EncryptedCredentials.de_json(data.get('credentials'), bot)
return cls(bot=bot, **data)
- def to_dict(self):
- data = super(PassportData, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
data['data'] = [e.to_dict() for e in self.data]
return data
@property
- def decrypted_data(self):
+ def decrypted_data(self) -> List[EncryptedPassportElement]:
"""
List[:class:`telegram.EncryptedPassportElement`]: Lazily decrypt and return information
about documents and other Telegram Passport elements which were shared with the bot.
@@ -84,15 +100,15 @@ def decrypted_data(self):
"""
if self._decrypted_data is None:
self._decrypted_data = [
- EncryptedPassportElement.de_json_decrypted(element.to_dict(),
- self.bot,
- self.decrypted_credentials)
+ EncryptedPassportElement.de_json_decrypted(
+ element.to_dict(), self.bot, self.decrypted_credentials
+ )
for element in self.data
]
return self._decrypted_data
@property
- def decrypted_credentials(self):
+ def decrypted_credentials(self) -> 'Credentials':
"""
:class:`telegram.Credentials`: Lazily decrypt and return credentials that were used
to decrypt the data. This object also contains the user specified payload as
diff --git a/telegramer/include/telegram/passport/passportelementerrors.py b/telegramer/include/telegram/passport/passportelementerrors.py
index d69935b..fd38ff3 100644
--- a/telegramer/include/telegram/passport/passportelementerrors.py
+++ b/telegramer/include/telegram/passport/passportelementerrors.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,27 +16,39 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=W0622
"""This module contains the classes that represent Telegram PassportElementError."""
+from typing import Any
+
from telegram import TelegramObject
class PassportElementError(TelegramObject):
"""Baseclass for the PassportElementError* classes.
- Attributes:
- source (:obj:`str`): Error source.
- type (:obj:`str`): The section of the user's Telegram Passport which has the error.
- message (:obj:`str`): Error message
+ This object represents an error in the Telegram Passport element which was submitted that
+ should be resolved by the user.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source` and :attr:`type` are equal.
Args:
source (:obj:`str`): Error source.
type (:obj:`str`): The section of the user's Telegram Passport which has the error.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ source (:obj:`str`): Error source.
+ type (:obj:`str`): The section of the user's Telegram Passport which has the error.
+ message (:obj:`str`): Error message.
+
"""
- def __init__(self, source, type, message, **kwargs):
+ # All subclasses of this class won't have _id_attrs in slots since it's added here.
+ __slots__ = ('message', 'source', 'type', '_id_attrs')
+
+ def __init__(self, source: str, type: str, message: str, **_kwargs: Any):
# Required
self.source = str(source)
self.type = str(type)
@@ -50,33 +62,34 @@ class PassportElementErrorDataField(PassportElementError):
Represents an issue in one of the data fields that was provided by the user. The error is
considered resolved when the field's value changes.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source`, :attr:`type`, :attr:`field_name`, :attr:`data_hash`
+ and :attr:`message` are equal.
+
+ Args:
type (:obj:`str`): The section of the user's Telegram Passport which has the error, one of
- "personal_details", "passport", "driver_license", "identity_card", "internal_passport",
- "address".
+ ``"personal_details"``, ``"passport"``, ``"driver_license"``, ``"identity_card"``,
+ ``"internal_passport"``, ``"address"``.
field_name (:obj:`str`): Name of the data field which has the error.
data_hash (:obj:`str`): Base64-encoded data hash.
message (:obj:`str`): Error message.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
type (:obj:`str`): The section of the user's Telegram Passport which has the error, one of
- "personal_details", "passport", "driver_license", "identity_card", "internal_passport",
- "address".
+ ``"personal_details"``, ``"passport"``, ``"driver_license"``, ``"identity_card"``,
+ ``"internal_passport"``, ``"address"``.
field_name (:obj:`str`): Name of the data field which has the error.
data_hash (:obj:`str`): Base64-encoded data hash.
message (:obj:`str`): Error message.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- type,
- field_name,
- data_hash,
- message,
- **kwargs):
+ __slots__ = ('data_hash', 'field_name')
+
+ def __init__(self, type: str, field_name: str, data_hash: str, message: str, **_kwargs: Any):
# Required
- super(PassportElementErrorDataField, self).__init__('data', type, message)
+ super().__init__('data', type, message)
self.field_name = field_name
self.data_hash = data_hash
@@ -88,30 +101,32 @@ class PassportElementErrorFile(PassportElementError):
Represents an issue with a document scan. The error is considered resolved when the file with
the document scan changes.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and
+ :attr:`message` are equal.
+
+ Args:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "utility_bill", "bank_statement", "rental_agreement", "passport_registration",
- "temporary_registration".
+ ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``,
+ ``"passport_registration"``, ``"temporary_registration"``.
file_hash (:obj:`str`): Base64-encoded file hash.
message (:obj:`str`): Error message.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "utility_bill", "bank_statement", "rental_agreement", "passport_registration",
- "temporary_registration".
+ ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``,
+ ``"passport_registration"``, ``"temporary_registration"``.
file_hash (:obj:`str`): Base64-encoded file hash.
message (:obj:`str`): Error message.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- type,
- file_hash,
- message,
- **kwargs):
+ __slots__ = ('file_hash',)
+
+ def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any):
# Required
- super(PassportElementErrorFile, self).__init__('file', type, message)
+ super().__init__('file', type, message)
self.file_hash = file_hash
self._id_attrs = (self.source, self.type, self.file_hash, self.message)
@@ -119,37 +134,38 @@ def __init__(self,
class PassportElementErrorFiles(PassportElementError):
"""
- Represents an issue with a list of scans. The error is considered resolved when the file with
- the document scan changes.
+ Represents an issue with a list of scans. The error is considered resolved when the list of
+ files with the document scans changes.
- Attributes:
- type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "utility_bill", "bank_statement", "rental_agreement", "passport_registration",
- "temporary_registration".
- file_hash (:obj:`str`): Base64-encoded file hash.
- message (:obj:`str`): Error message.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hashes`, and
+ :attr:`message` are equal.
Args:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "utility_bill", "bank_statement", "rental_agreement", "passport_registration",
- "temporary_registration".
+ ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``,
+ ``"passport_registration"``, ``"temporary_registration"``.
file_hashes (List[:obj:`str`]): List of base64-encoded file hashes.
message (:obj:`str`): Error message.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
+ ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``,
+ ``"passport_registration"``, ``"temporary_registration"``.
+ file_hashes (List[:obj:`str`]): List of base64-encoded file hashes.
+ message (:obj:`str`): Error message.
+
"""
- def __init__(self,
- type,
- file_hashes,
- message,
- **kwargs):
+ __slots__ = ('file_hashes',)
+
+ def __init__(self, type: str, file_hashes: str, message: str, **_kwargs: Any):
# Required
- super(PassportElementErrorFiles, self).__init__('files', type, message)
+ super().__init__('files', type, message)
self.file_hashes = file_hashes
- self._id_attrs = ((self.source, self.type, self.message) +
- tuple([file_hash for file_hash in file_hashes]))
+ self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes)
class PassportElementErrorFrontSide(PassportElementError):
@@ -157,30 +173,32 @@ class PassportElementErrorFrontSide(PassportElementError):
Represents an issue with the front side of a document. The error is considered resolved when
the file with the front side of the document changes.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and
+ :attr:`message` are equal.
+
+ Args:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "passport", "driver_license", "identity_card", "internal_passport".
+ ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``.
file_hash (:obj:`str`): Base64-encoded hash of the file with the front side of the
document.
message (:obj:`str`): Error message.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "passport", "driver_license", "identity_card", "internal_passport".
+ ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``.
file_hash (:obj:`str`): Base64-encoded hash of the file with the front side of the
document.
message (:obj:`str`): Error message.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- type,
- file_hash,
- message,
- **kwargs):
+ __slots__ = ('file_hash',)
+
+ def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any):
# Required
- super(PassportElementErrorFrontSide, self).__init__('front_side', type, message)
+ super().__init__('front_side', type, message)
self.file_hash = file_hash
self._id_attrs = (self.source, self.type, self.file_hash, self.message)
@@ -188,33 +206,35 @@ def __init__(self,
class PassportElementErrorReverseSide(PassportElementError):
"""
- Represents an issue with the front side of a document. The error is considered resolved when
+ Represents an issue with the reverse side of a document. The error is considered resolved when
the file with the reverse side of the document changes.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and
+ :attr:`message` are equal.
+
+ Args:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "passport", "driver_license", "identity_card", "internal_passport".
+ ``"driver_license"``, ``"identity_card"``.
file_hash (:obj:`str`): Base64-encoded hash of the file with the reverse side of the
document.
message (:obj:`str`): Error message.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "driver_license", "identity_card".
+ ``"driver_license"``, ``"identity_card"``.
file_hash (:obj:`str`): Base64-encoded hash of the file with the reverse side of the
document.
message (:obj:`str`): Error message.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- type,
- file_hash,
- message,
- **kwargs):
+ __slots__ = ('file_hash',)
+
+ def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any):
# Required
- super(PassportElementErrorReverseSide, self).__init__('reverse_side', type, message)
+ super().__init__('reverse_side', type, message)
self.file_hash = file_hash
self._id_attrs = (self.source, self.type, self.file_hash, self.message)
@@ -225,28 +245,30 @@ class PassportElementErrorSelfie(PassportElementError):
Represents an issue with the selfie with a document. The error is considered resolved when
the file with the selfie changes.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and
+ :attr:`message` are equal.
+
+ Args:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "passport", "driver_license", "identity_card", "internal_passport".
+ ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``.
file_hash (:obj:`str`): Base64-encoded hash of the file with the selfie.
message (:obj:`str`): Error message.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
- "passport", "driver_license", "identity_card", "internal_passport".
+ ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``.
file_hash (:obj:`str`): Base64-encoded hash of the file with the selfie.
message (:obj:`str`): Error message.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- type,
- file_hash,
- message,
- **kwargs):
+ __slots__ = ('file_hash',)
+
+ def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any):
# Required
- super(PassportElementErrorSelfie, self).__init__('selfie', type, message)
+ super().__init__('selfie', type, message)
self.file_hash = file_hash
self._id_attrs = (self.source, self.type, self.file_hash, self.message)
@@ -257,33 +279,34 @@ class PassportElementErrorTranslationFile(PassportElementError):
Represents an issue with one of the files that constitute the translation of a document.
The error is considered resolved when the file changes.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hash`, and
+ :attr:`message` are equal.
+
+ Args:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue,
- one of "passport", "driver_license", "identity_card", "internal_passport",
- "utility_bill", "bank_statement", "rental_agreement", "passport_registration",
- "temporary_registration".
+ one of ``"passport"``, ``"driver_license"``, ``"identity_card"``,
+ ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``,
+ ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``.
file_hash (:obj:`str`): Base64-encoded hash of the file.
message (:obj:`str`): Error message.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue,
- one of "passport", "driver_license", "identity_card", "internal_passport",
- "utility_bill", "bank_statement", "rental_agreement", "passport_registration",
- "temporary_registration".
+ one of ``"passport"``, ``"driver_license"``, ``"identity_card"``,
+ ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``,
+ ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``.
file_hash (:obj:`str`): Base64-encoded hash of the file.
message (:obj:`str`): Error message.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- type,
- file_hash,
- message,
- **kwargs):
+ __slots__ = ('file_hash',)
+
+ def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any):
# Required
- super(PassportElementErrorTranslationFile, self).__init__('translation_file',
- type, message)
+ super().__init__('translation_file', type, message)
self.file_hash = file_hash
self._id_attrs = (self.source, self.type, self.file_hash, self.message)
@@ -292,39 +315,39 @@ def __init__(self,
class PassportElementErrorTranslationFiles(PassportElementError):
"""
Represents an issue with the translated version of a document. The error is considered
- resolved when a file with the document translation change.
+ resolved when a file with the document translation changes.
- Attributes:
- type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue,
- one of "passport", "driver_license", "identity_card", "internal_passport",
- "utility_bill", "bank_statement", "rental_agreement", "passport_registration",
- "temporary_registration"
- file_hash (:obj:`str`): Base64-encoded file hash.
- message (:obj:`str`): Error message.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source`, :attr:`type`, :attr:`file_hashes`, and
+ :attr:`message` are equal.
Args:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue,
- one of "passport", "driver_license", "identity_card", "internal_passport",
- "utility_bill", "bank_statement", "rental_agreement", "passport_registration",
- "temporary_registration"
+ one of ``"passport"``, ``"driver_license"``, ``"identity_card"``,
+ ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``,
+ ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``.
file_hashes (List[:obj:`str`]): List of base64-encoded file hashes.
message (:obj:`str`): Error message.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue,
+ one of ``"passport"``, ``"driver_license"``, ``"identity_card"``,
+ ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``,
+ ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``.
+ file_hashes (List[:obj:`str`]): List of base64-encoded file hashes.
+ message (:obj:`str`): Error message.
+
"""
- def __init__(self,
- type,
- file_hashes,
- message,
- **kwargs):
+ __slots__ = ('file_hashes',)
+
+ def __init__(self, type: str, file_hashes: str, message: str, **_kwargs: Any):
# Required
- super(PassportElementErrorTranslationFiles, self).__init__('translation_files',
- type, message)
+ super().__init__('translation_files', type, message)
self.file_hashes = file_hashes
- self._id_attrs = ((self.source, self.type, self.message) +
- tuple([file_hash for file_hash in file_hashes]))
+ self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes)
class PassportElementErrorUnspecified(PassportElementError):
@@ -332,26 +355,28 @@ class PassportElementErrorUnspecified(PassportElementError):
Represents an issue in an unspecified place. The error is considered resolved when new
data is added.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`source`, :attr:`type`, :attr:`element_hash`,
+ and :attr:`message` are equal.
+
+ Args:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue.
element_hash (:obj:`str`): Base64-encoded element hash.
message (:obj:`str`): Error message.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue.
element_hash (:obj:`str`): Base64-encoded element hash.
message (:obj:`str`): Error message.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- type,
- element_hash,
- message,
- **kwargs):
+ __slots__ = ('element_hash',)
+
+ def __init__(self, type: str, element_hash: str, message: str, **_kwargs: Any):
# Required
- super(PassportElementErrorUnspecified, self).__init__('unspecified', type, message)
+ super().__init__('unspecified', type, message)
self.element_hash = element_hash
self._id_attrs = (self.source, self.type, self.element_hash, self.message)
diff --git a/telegramer/include/telegram/passport/passportfile.py b/telegramer/include/telegram/passport/passportfile.py
index 4b82294..59f60c8 100644
--- a/telegramer/include/telegram/passport/passportfile.py
+++ b/telegramer/include/telegram/passport/passportfile.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,14 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Encrypted PassportFile."""
+from typing import TYPE_CHECKING, Any, List, Optional
+
from telegram import TelegramObject
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot, File, FileCredentials
class PassportFile(TelegramObject):
@@ -26,86 +33,128 @@ class PassportFile(TelegramObject):
This object represents a file uploaded to Telegram Passport. Currently all Telegram Passport
files are in JPEG format when decrypted and don't exceed 10MB.
- Attributes:
- file_id (:obj:`str`): Unique identifier for this file.
- file_size (:obj:`int`): File size.
- file_date (:obj:`int`): Unix time when the file was uploaded.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`file_unique_id` is equal.
Args:
- file_id (:obj:`str`): Unique identifier for this file.
+ file_id (:obj:`str`): Identifier for this file, which can be used to download
+ or reuse the file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
file_size (:obj:`int`): File size.
file_date (:obj:`int`): Unix time when the file was uploaded.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ file_id (:obj:`str`): Identifier for this file.
+ file_unique_id (:obj:`str`): Unique identifier for this file, which
+ is supposed to be the same over time and for different bots.
+ Can't be used to download or reuse the file.
+ file_size (:obj:`int`): File size.
+ file_date (:obj:`int`): Unix time when the file was uploaded.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+
"""
- def __init__(self, file_id, file_date, file_size=None, bot=None, credentials=None, **kwargs):
+ __slots__ = (
+ 'file_date',
+ 'bot',
+ 'file_id',
+ 'file_size',
+ '_credentials',
+ 'file_unique_id',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ file_id: str,
+ file_unique_id: str,
+ file_date: int,
+ file_size: int = None,
+ bot: 'Bot' = None,
+ credentials: 'FileCredentials' = None,
+ **_kwargs: Any,
+ ):
# Required
self.file_id = file_id
+ self.file_unique_id = file_unique_id
self.file_size = file_size
self.file_date = file_date
# Optionals
self.bot = bot
self._credentials = credentials
- self._id_attrs = (self.file_id,)
+ self._id_attrs = (self.file_unique_id,)
@classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
+ def de_json_decrypted(
+ cls, data: Optional[JSONDict], bot: 'Bot', credentials: 'FileCredentials'
+ ) -> Optional['PassportFile']:
+ """Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account
+ passport credentials.
+
+ Args:
+ data (Dict[:obj:`str`, ...]): The JSON data.
+ bot (:class:`telegram.Bot`): The bot associated with this object.
+ credentials (:class:`telegram.FileCredentials`): The credentials
- data = super(PassportFile, cls).de_json(data, bot)
+ Returns:
+ :class:`telegram.PassportFile`:
- return cls(bot=bot, **data)
+ """
+ data = cls._parse_data(data)
- @classmethod
- def de_json_decrypted(cls, data, bot, credentials):
if not data:
return None
- data = super(PassportFile, cls).de_json(data, bot)
-
data['credentials'] = credentials
return cls(bot=bot, **data)
@classmethod
- def de_list(cls, data, bot):
- if not data:
- return []
+ def de_list_decrypted(
+ cls, data: Optional[List[JSONDict]], bot: 'Bot', credentials: List['FileCredentials']
+ ) -> List[Optional['PassportFile']]:
+ """Variant of :meth:`telegram.TelegramObject.de_list` that also takes into account
+ passport credentials.
- return [cls.de_json(passport_file, bot) for passport_file in data]
+ Args:
+ data (Dict[:obj:`str`, ...]): The JSON data.
+ bot (:class:`telegram.Bot`): The bot associated with these objects.
+ credentials (:class:`telegram.FileCredentials`): The credentials
- @classmethod
- def de_list_decrypted(cls, data, bot, credentials):
+ Returns:
+ List[:class:`telegram.PassportFile`]:
+
+ """
if not data:
return []
- return [cls.de_json_decrypted(passport_file, bot, credentials[i])
- for i, passport_file in enumerate(data)]
+ return [
+ cls.de_json_decrypted(passport_file, bot, credentials[i])
+ for i, passport_file in enumerate(data)
+ ]
- def get_file(self, timeout=None, **kwargs):
+ def get_file(
+ self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
+ ) -> 'File':
"""
Wrapper over :attr:`telegram.Bot.get_file`. Will automatically assign the correct
credentials to the returned :class:`telegram.File` if originating from
:obj:`telegram.PassportData.decrypted_data`.
- Args:
- timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
- the read timeout from the server (instead of the one specified during creation of
- the connection pool).
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`.
Returns:
:class:`telegram.File`
Raises:
- :class:`telegram.TelegramError`
+ :class:`telegram.error.TelegramError`
"""
- file = self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
+ file = self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs)
file.set_credentials(self._credentials)
return file
diff --git a/telegramer/include/telegram/payment/invoice.py b/telegramer/include/telegram/payment/invoice.py
index cefc35a..18fda43 100644
--- a/telegramer/include/telegram/payment/invoice.py
+++ b/telegramer/include/telegram/payment/invoice.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,18 +18,17 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Invoice."""
+from typing import Any
+
from telegram import TelegramObject
class Invoice(TelegramObject):
"""This object contains basic information about an invoice.
- Attributes:
- title (:obj:`str`): Product name.
- description (:obj:`str`): Product description.
- start_parameter (:obj:`str`): Unique bot deep-linking parameter.
- currency (:obj:`str`): Three-letter ISO 4217 currency code.
- total_amount (:obj:`int`): Total price in the smallest units of the currency.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`title`, :attr:`description`, :attr:`start_parameter`,
+ :attr:`currency` and :attr:`total_amount` are equal.
Args:
title (:obj:`str`): Product name.
@@ -38,21 +37,50 @@ class Invoice(TelegramObject):
generate this invoice.
currency (:obj:`str`): Three-letter ISO 4217 currency code.
total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not
- float/double). For example, for a price of US$ 1.45 pass amount = 145.
+ float/double). For example, for a price of US$ 1.45 pass ``amount = 145``. See the
+ :obj:`exp` parameter in
+ `currencies.json `_,
+ it shows the number of digits past the decimal point for each currency
+ (2 for the majority of currencies).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ title (:obj:`str`): Product name.
+ description (:obj:`str`): Product description.
+ start_parameter (:obj:`str`): Unique bot deep-linking parameter.
+ currency (:obj:`str`): Three-letter ISO 4217 currency code.
+ total_amount (:obj:`int`): Total price in the smallest units of the currency.
+
"""
- def __init__(self, title, description, start_parameter, currency, total_amount, **kwargs):
+ __slots__ = (
+ 'currency',
+ 'start_parameter',
+ 'title',
+ 'description',
+ 'total_amount',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ title: str,
+ description: str,
+ start_parameter: str,
+ currency: str,
+ total_amount: int,
+ **_kwargs: Any,
+ ):
self.title = title
self.description = description
self.start_parameter = start_parameter
self.currency = currency
self.total_amount = total_amount
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- return cls(**data)
+ self._id_attrs = (
+ self.title,
+ self.description,
+ self.start_parameter,
+ self.currency,
+ self.total_amount,
+ )
diff --git a/telegramer/include/telegram/payment/labeledprice.py b/telegramer/include/telegram/payment/labeledprice.py
index 5ee3c27..37b58e3 100644
--- a/telegramer/include/telegram/payment/labeledprice.py
+++ b/telegramer/include/telegram/payment/labeledprice.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,26 +18,37 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram LabeledPrice."""
+from typing import Any
+
from telegram import TelegramObject
class LabeledPrice(TelegramObject):
"""This object represents a portion of the price for goods or services.
- Attributes:
- label (:obj:`str`): Portion label.
- amount (:obj:`int`): Price of the product in the smallest units of the currency.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`label` and :attr:`amount` are equal.
Args:
- label (:obj:`str`): Portion label
+ label (:obj:`str`): Portion label.
amount (:obj:`int`): Price of the product in the smallest units of the currency (integer,
- not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp
- parameter in currencies.json, it shows the number of digits past the decimal point for
- each currency (2 for the majority of currencies).
+ not float/double). For example, for a price of US$ 1.45 pass ``amount = 145``.
+ See the :obj:`exp` parameter in
+ `currencies.json `_,
+ it shows the number of digits past the decimal point for each currency
+ (2 for the majority of currencies).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ label (:obj:`str`): Portion label.
+ amount (:obj:`int`): Price of the product in the smallest units of the currency.
+
"""
- def __init__(self, label, amount, **kwargs):
+ __slots__ = ('label', '_id_attrs', 'amount')
+
+ def __init__(self, label: str, amount: int, **_kwargs: Any):
self.label = label
self.amount = amount
+
+ self._id_attrs = (self.label, self.amount)
diff --git a/telegramer/include/telegram/payment/orderinfo.py b/telegramer/include/telegram/payment/orderinfo.py
index 21f8657..902b324 100644
--- a/telegramer/include/telegram/payment/orderinfo.py
+++ b/telegramer/include/telegram/payment/orderinfo.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,17 +18,21 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram OrderInfo."""
-from telegram import TelegramObject, ShippingAddress
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import ShippingAddress, TelegramObject
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class OrderInfo(TelegramObject):
"""This object represents information about an order.
- Attributes:
- name (:obj:`str`): Optional. User name.
- phone_number (:obj:`str`): Optional. User's phone number.
- email (:obj:`str`): Optional. User email.
- shipping_address (:class:`telegram.ShippingAddress`): Optional. User shipping address.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`name`, :attr:`phone_number`, :attr:`email` and
+ :attr:`shipping_address` are equal.
Args:
name (:obj:`str`, optional): User name.
@@ -37,21 +41,39 @@ class OrderInfo(TelegramObject):
shipping_address (:class:`telegram.ShippingAddress`, optional): User shipping address.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ name (:obj:`str`): Optional. User name.
+ phone_number (:obj:`str`): Optional. User's phone number.
+ email (:obj:`str`): Optional. User email.
+ shipping_address (:class:`telegram.ShippingAddress`): Optional. User shipping address.
+
"""
- def __init__(self, name=None, phone_number=None, email=None, shipping_address=None, **kwargs):
+ __slots__ = ('email', 'shipping_address', 'phone_number', 'name', '_id_attrs')
+
+ def __init__(
+ self,
+ name: str = None,
+ phone_number: str = None,
+ email: str = None,
+ shipping_address: str = None,
+ **_kwargs: Any,
+ ):
self.name = name
self.phone_number = phone_number
self.email = email
self.shipping_address = shipping_address
+ self._id_attrs = (self.name, self.phone_number, self.email, self.shipping_address)
+
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['OrderInfo']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return cls()
- data = super(OrderInfo, cls).de_json(data, bot)
-
data['shipping_address'] = ShippingAddress.de_json(data.get('shipping_address'), bot)
return cls(**data)
diff --git a/telegramer/include/telegram/payment/precheckoutquery.py b/telegramer/include/telegram/payment/precheckoutquery.py
index 36eebf6..65b5e30 100644
--- a/telegramer/include/telegram/payment/precheckoutquery.py
+++ b/telegramer/include/telegram/payment/precheckoutquery.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,34 +18,35 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram PreCheckoutQuery."""
-from telegram import TelegramObject, User, OrderInfo
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import OrderInfo, TelegramObject, User
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot
class PreCheckoutQuery(TelegramObject):
"""This object contains information about an incoming pre-checkout query.
- Note:
- * In Python `from` is a reserved word, use `from_user` instead.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
- Attributes:
- id (:obj:`str`): Unique query identifier.
- from_user (:class:`telegram.User`): User who sent the query.
- currency (:obj:`str`): Three-letter ISO 4217 currency code.
- total_amount (:obj:`int`): Total price in the smallest units of the currency.
- invoice_payload (:obj:`str`): Bot specified invoice payload.
- shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the
- user.
- order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ Note:
+ In Python ``from`` is a reserved word, use ``from_user`` instead.
Args:
id (:obj:`str`): Unique query identifier.
from_user (:class:`telegram.User`): User who sent the query.
- currency (:obj:`str`): Three-letter ISO 4217 currency code
+ currency (:obj:`str`): Three-letter ISO 4217 currency code.
total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not
- float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp
- parameter in currencies.json, it shows the number of digits past the decimal point for
- each currency (2 for the majority of currencies).
+ float/double). For example, for a price of US$ 1.45 pass ``amount = 145``.
+ See the :obj:`exp` parameter in
+ `currencies.json `_,
+ it shows the number of digits past the decimal point for each currency
+ (2 for the majority of currencies).
invoice_payload (:obj:`str`): Bot specified invoice payload.
shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the
user.
@@ -53,19 +54,44 @@ class PreCheckoutQuery(TelegramObject):
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ id (:obj:`str`): Unique query identifier.
+ from_user (:class:`telegram.User`): User who sent the query.
+ currency (:obj:`str`): Three-letter ISO 4217 currency code.
+ total_amount (:obj:`int`): Total price in the smallest units of the currency.
+ invoice_payload (:obj:`str`): Bot specified invoice payload.
+ shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the
+ user.
+ order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+
"""
- def __init__(self,
- id,
- from_user,
- currency,
- total_amount,
- invoice_payload,
- shipping_option_id=None,
- order_info=None,
- bot=None,
- **kwargs):
- self.id = id
+ __slots__ = (
+ 'bot',
+ 'invoice_payload',
+ 'shipping_option_id',
+ 'currency',
+ 'order_info',
+ 'total_amount',
+ 'id',
+ 'from_user',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ from_user: User,
+ currency: str,
+ total_amount: int,
+ invoice_payload: str,
+ shipping_option_id: str = None,
+ order_info: OrderInfo = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
+ self.id = id # pylint: disable=C0103
self.from_user = from_user
self.currency = currency
self.total_amount = total_amount
@@ -78,31 +104,37 @@ def __init__(self,
self._id_attrs = (self.id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['PreCheckoutQuery']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(PreCheckoutQuery, cls).de_json(data, bot)
-
data['from_user'] = User.de_json(data.pop('from'), bot)
data['order_info'] = OrderInfo.de_json(data.get('order_info'), bot)
return cls(bot=bot, **data)
- def answer(self, *args, **kwargs):
+ def answer( # pylint: disable=C0103
+ self,
+ ok: bool,
+ error_message: str = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Shortcut for::
bot.answer_pre_checkout_query(update.pre_checkout_query.id, *args, **kwargs)
- Args:
- ok (:obj:`bool`): Specify True if everything is alright (goods are available, etc.) and
- the bot is ready to proceed with the order. Use False if there are any problems.
- error_message (:obj:`str`, optional): Required if ok is False. Error message in human
- readable form that explains the reason for failure to proceed with the checkout
- (e.g. "Sorry, somebody just bought the last of our amazing black T-shirts while you
- were busy filling out your payment details. Please choose a different color or
- garment!"). Telegram will display this message to the user.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.answer_pre_checkout_query`.
"""
- return self.bot.answer_pre_checkout_query(self.id, *args, **kwargs)
+ return self.bot.answer_pre_checkout_query(
+ pre_checkout_query_id=self.id,
+ ok=ok,
+ error_message=error_message,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
diff --git a/telegramer/include/telegram/payment/shippingaddress.py b/telegramer/include/telegram/payment/shippingaddress.py
index 6606605..ae16d9f 100644
--- a/telegramer/include/telegram/payment/shippingaddress.py
+++ b/telegramer/include/telegram/payment/shippingaddress.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,32 +18,57 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ShippingAddress."""
+from typing import Any
+
from telegram import TelegramObject
class ShippingAddress(TelegramObject):
"""This object represents a Telegram ShippingAddress.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`country_code`, :attr:`state`, :attr:`city`,
+ :attr:`street_line1`, :attr:`street_line2` and :attr:`post_cod` are equal.
+
+ Args:
country_code (:obj:`str`): ISO 3166-1 alpha-2 country code.
state (:obj:`str`): State, if applicable.
city (:obj:`str`): City.
street_line1 (:obj:`str`): First line for the address.
street_line2 (:obj:`str`): Second line for the address.
post_code (:obj:`str`): Address post code.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
country_code (:obj:`str`): ISO 3166-1 alpha-2 country code.
state (:obj:`str`): State, if applicable.
city (:obj:`str`): City.
street_line1 (:obj:`str`): First line for the address.
street_line2 (:obj:`str`): Second line for the address.
post_code (:obj:`str`): Address post code.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self, country_code, state, city, street_line1, street_line2, post_code, **kwargs):
+ __slots__ = (
+ 'post_code',
+ 'city',
+ '_id_attrs',
+ 'country_code',
+ 'street_line2',
+ 'street_line1',
+ 'state',
+ )
+
+ def __init__(
+ self,
+ country_code: str,
+ state: str,
+ city: str,
+ street_line1: str,
+ street_line2: str,
+ post_code: str,
+ **_kwargs: Any,
+ ):
self.country_code = country_code
self.state = state
self.city = city
@@ -51,12 +76,11 @@ def __init__(self, country_code, state, city, street_line1, street_line2, post_c
self.street_line2 = street_line2
self.post_code = post_code
- self._id_attrs = (self.country_code, self.state, self.city, self.street_line1,
- self.street_line2, self.post_code)
-
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- return cls(**data)
+ self._id_attrs = (
+ self.country_code,
+ self.state,
+ self.city,
+ self.street_line1,
+ self.street_line2,
+ self.post_code,
+ )
diff --git a/telegramer/include/telegram/payment/shippingoption.py b/telegramer/include/telegram/payment/shippingoption.py
index 0695c41..5bca48a 100644
--- a/telegramer/include/telegram/payment/shippingoption.py
+++ b/telegramer/include/telegram/payment/shippingoption.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,34 +18,52 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ShippingOption."""
+from typing import TYPE_CHECKING, Any, List
+
from telegram import TelegramObject
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import LabeledPrice # noqa
class ShippingOption(TelegramObject):
"""This object represents one shipping option.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
+
+ Args:
id (:obj:`str`): Shipping option identifier.
title (:obj:`str`): Option title.
prices (List[:class:`telegram.LabeledPrice`]): List of price portions.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
id (:obj:`str`): Shipping option identifier.
title (:obj:`str`): Option title.
prices (List[:class:`telegram.LabeledPrice`]): List of price portions.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self, id, title, prices, **kwargs):
- self.id = id
+ __slots__ = ('prices', 'title', 'id', '_id_attrs')
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ title: str,
+ prices: List['LabeledPrice'],
+ **_kwargs: Any,
+ ):
+ self.id = id # pylint: disable=C0103
self.title = title
self.prices = prices
self._id_attrs = (self.id,)
- def to_dict(self):
- data = super(ShippingOption, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
data['prices'] = [p.to_dict() for p in self.prices]
diff --git a/telegramer/include/telegram/payment/shippingquery.py b/telegramer/include/telegram/payment/shippingquery.py
index a5b8191..2a0d140 100644
--- a/telegramer/include/telegram/payment/shippingquery.py
+++ b/telegramer/include/telegram/payment/shippingquery.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,34 +18,54 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ShippingQuery."""
-from telegram import TelegramObject, User, ShippingAddress
+from typing import TYPE_CHECKING, Any, Optional, List
+
+from telegram import ShippingAddress, TelegramObject, User, ShippingOption
+from telegram.utils.helpers import DEFAULT_NONE
+from telegram.utils.types import JSONDict, ODVInput
+
+if TYPE_CHECKING:
+ from telegram import Bot
class ShippingQuery(TelegramObject):
"""This object contains information about an incoming shipping query.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
+
Note:
- * In Python `from` is a reserved word, use `from_user` instead.
+ In Python ``from`` is a reserved word, use ``from_user`` instead.
- Attributes:
+ Args:
id (:obj:`str`): Unique query identifier.
from_user (:class:`telegram.User`): User who sent the query.
invoice_payload (:obj:`str`): Bot specified invoice payload.
shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address.
- bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
+ bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
id (:obj:`str`): Unique query identifier.
from_user (:class:`telegram.User`): User who sent the query.
invoice_payload (:obj:`str`): Bot specified invoice payload.
shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address.
- bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
"""
- def __init__(self, id, from_user, invoice_payload, shipping_address, bot=None, **kwargs):
- self.id = id
+ __slots__ = ('bot', 'invoice_payload', 'shipping_address', 'id', 'from_user', '_id_attrs')
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ from_user: User,
+ invoice_payload: str,
+ shipping_address: ShippingAddress,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
+ self.id = id # pylint: disable=C0103
self.from_user = from_user
self.invoice_payload = invoice_payload
self.shipping_address = shipping_address
@@ -55,32 +75,39 @@ def __init__(self, id, from_user, invoice_payload, shipping_address, bot=None, *
self._id_attrs = (self.id,)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ShippingQuery']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(ShippingQuery, cls).de_json(data, bot)
-
data['from_user'] = User.de_json(data.pop('from'), bot)
data['shipping_address'] = ShippingAddress.de_json(data.get('shipping_address'), bot)
- return cls(**data)
+ return cls(bot=bot, **data)
- def answer(self, *args, **kwargs):
+ def answer( # pylint: disable=C0103
+ self,
+ ok: bool,
+ shipping_options: List[ShippingOption] = None,
+ error_message: str = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
"""Shortcut for::
bot.answer_shipping_query(update.shipping_query.id, *args, **kwargs)
- Args:
- ok (:obj:`bool`): Specify True if delivery to the specified address is possible and
- False if there are any problems (for example, if delivery to the specified address
- is not possible).
- shipping_options (List[:class:`telegram.ShippingOption`], optional): Required if ok is
- True. A JSON-serialized array of available shipping options.
- error_message (:obj:`str`, optional): Required if ok is False. Error message in human
- readable form that explains why it is impossible to complete the order (e.g.
- "Sorry, delivery to your desired address is unavailable'). Telegram will display
- this message to the user.
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.answer_shipping_query`.
"""
- return self.bot.answer_shipping_query(self.id, *args, **kwargs)
+ return self.bot.answer_shipping_query(
+ shipping_query_id=self.id,
+ ok=ok,
+ shipping_options=shipping_options,
+ error_message=error_message,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
diff --git a/telegramer/include/telegram/payment/successfulpayment.py b/telegramer/include/telegram/payment/successfulpayment.py
index bdda7be..7240fb8 100644
--- a/telegramer/include/telegram/payment/successfulpayment.py
+++ b/telegramer/include/telegram/payment/successfulpayment.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,47 +18,72 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram SuccessfulPayment."""
-from telegram import TelegramObject, OrderInfo
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import OrderInfo, TelegramObject
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class SuccessfulPayment(TelegramObject):
"""This object contains basic information about a successful payment.
- Attributes:
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`telegram_payment_charge_id` and
+ :attr:`provider_payment_charge_id` are equal.
+
+ Args:
currency (:obj:`str`): Three-letter ISO 4217 currency code.
- total_amount (:obj:`int`): Total price in the smallest units of the currency.
+ total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not
+ float/double). For example, for a price of US$ 1.45 pass ``amount = 145``.
+ See the :obj:`exp` parameter in
+ `currencies.json `_,
+ it shows the number of digits past the decimal point for each currency
+ (2 for the majority of currencies).
invoice_payload (:obj:`str`): Bot specified invoice payload.
- shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the
+ shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the
user.
- order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user.
+ order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user.
telegram_payment_charge_id (:obj:`str`): Telegram payment identifier.
provider_payment_charge_id (:obj:`str`): Provider payment identifier.
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
- Args:
+ Attributes:
currency (:obj:`str`): Three-letter ISO 4217 currency code.
- total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not
- float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp
- parameter in currencies.json, it shows the number of digits past the decimal point for
- each currency (2 for the majority of currencies).
+ total_amount (:obj:`int`): Total price in the smallest units of the currency.
invoice_payload (:obj:`str`): Bot specified invoice payload.
- shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the
+ shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the
user.
- order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user
+ order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user.
telegram_payment_charge_id (:obj:`str`): Telegram payment identifier.
provider_payment_charge_id (:obj:`str`): Provider payment identifier.
- **kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
- def __init__(self,
- currency,
- total_amount,
- invoice_payload,
- telegram_payment_charge_id,
- provider_payment_charge_id,
- shipping_option_id=None,
- order_info=None,
- **kwargs):
+ __slots__ = (
+ 'invoice_payload',
+ 'shipping_option_id',
+ 'currency',
+ 'order_info',
+ 'telegram_payment_charge_id',
+ 'provider_payment_charge_id',
+ 'total_amount',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ currency: str,
+ total_amount: int,
+ invoice_payload: str,
+ telegram_payment_charge_id: str,
+ provider_payment_charge_id: str,
+ shipping_option_id: str = None,
+ order_info: OrderInfo = None,
+ **_kwargs: Any,
+ ):
self.currency = currency
self.total_amount = total_amount
self.invoice_payload = invoice_payload
@@ -70,11 +95,13 @@ def __init__(self,
self._id_attrs = (self.telegram_payment_charge_id, self.provider_payment_charge_id)
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SuccessfulPayment']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(SuccessfulPayment, cls).de_json(data, bot)
data['order_info'] = OrderInfo.de_json(data.get('order_info'), bot)
return cls(**data)
diff --git a/telegramer/include/telegram/poll.py b/telegramer/include/telegram/poll.py
new file mode 100644
index 0000000..a7c51b2
--- /dev/null
+++ b/telegramer/include/telegram/poll.py
@@ -0,0 +1,295 @@
+#!/usr/bin/env python
+# pylint: disable=R0903
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram Poll."""
+
+import datetime
+import sys
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, ClassVar
+
+from telegram import MessageEntity, TelegramObject, User, constants
+from telegram.utils.helpers import from_timestamp, to_timestamp
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+
+class PollOption(TelegramObject):
+ """
+ This object contains information about one answer option in a poll.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`text` and :attr:`voter_count` are equal.
+
+ Args:
+ text (:obj:`str`): Option text, 1-100 characters.
+ voter_count (:obj:`int`): Number of users that voted for this option.
+
+ Attributes:
+ text (:obj:`str`): Option text, 1-100 characters.
+ voter_count (:obj:`int`): Number of users that voted for this option.
+
+ """
+
+ __slots__ = ('voter_count', 'text', '_id_attrs')
+
+ def __init__(self, text: str, voter_count: int, **_kwargs: Any):
+ self.text = text
+ self.voter_count = voter_count
+
+ self._id_attrs = (self.text, self.voter_count)
+
+ MAX_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH
+ """:const:`telegram.constants.MAX_POLL_OPTION_LENGTH`"""
+
+
+class PollAnswer(TelegramObject):
+ """
+ This object represents an answer of a user in a non-anonymous poll.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`poll_id`, :attr:`user` and :attr:`options_ids` are equal.
+
+ Attributes:
+ poll_id (:obj:`str`): Unique poll identifier.
+ user (:class:`telegram.User`): The user, who changed the answer to the poll.
+ option_ids (List[:obj:`int`]): Identifiers of answer options, chosen by the user.
+
+ Args:
+ poll_id (:obj:`str`): Unique poll identifier.
+ user (:class:`telegram.User`): The user, who changed the answer to the poll.
+ option_ids (List[:obj:`int`]): 0-based identifiers of answer options, chosen by the user.
+ May be empty if the user retracted their vote.
+
+ """
+
+ __slots__ = ('option_ids', 'user', 'poll_id', '_id_attrs')
+
+ def __init__(self, poll_id: str, user: User, option_ids: List[int], **_kwargs: Any):
+ self.poll_id = poll_id
+ self.user = user
+ self.option_ids = option_ids
+
+ self._id_attrs = (self.poll_id, self.user, tuple(self.option_ids))
+
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['PollAnswer']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ data['user'] = User.de_json(data.get('user'), bot)
+
+ return cls(**data)
+
+
+class Poll(TelegramObject):
+ """
+ This object contains information about a poll.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
+
+ Attributes:
+ id (:obj:`str`): Unique poll identifier.
+ question (:obj:`str`): Poll question, 1-300 characters.
+ options (List[:class:`PollOption`]): List of poll options.
+ total_voter_count (:obj:`int`): Total number of users that voted in the poll.
+ is_closed (:obj:`bool`): :obj:`True`, if the poll is closed.
+ is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous.
+ type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`.
+ allows_multiple_answers (:obj:`bool`): :obj:`True`, if the poll allows multiple answers.
+ correct_option_id (:obj:`int`): Optional. Identifier of the correct answer option.
+ explanation (:obj:`str`): Optional. Text that is shown when a user chooses an incorrect
+ answer or taps on the lamp icon in a quiz-style poll.
+ explanation_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities
+ like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`.
+ open_period (:obj:`int`): Optional. Amount of time in seconds the poll will be active
+ after creation.
+ close_date (:obj:`datetime.datetime`): Optional. Point in time when the poll will be
+ automatically closed.
+
+ Args:
+ id (:obj:`str`): Unique poll identifier.
+ question (:obj:`str`): Poll question, 1-300 characters.
+ options (List[:class:`PollOption`]): List of poll options.
+ is_closed (:obj:`bool`): :obj:`True`, if the poll is closed.
+ is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous.
+ type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`.
+ allows_multiple_answers (:obj:`bool`): :obj:`True`, if the poll allows multiple answers.
+ correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer option.
+ Available only for polls in the quiz mode, which are closed, or was sent (not
+ forwarded) by the bot or to the private chat with the bot.
+ explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect
+ answer or taps on the lamp icon in a quiz-style poll, 0-200 characters.
+ explanation_entities (List[:class:`telegram.MessageEntity`], optional): Special entities
+ like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`.
+ open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active
+ after creation.
+ close_date (:obj:`datetime.datetime`, optional): Point in time (Unix timestamp) when the
+ poll will be automatically closed. Converted to :obj:`datetime.datetime`.
+
+ """
+
+ __slots__ = (
+ 'total_voter_count',
+ 'allows_multiple_answers',
+ 'open_period',
+ 'options',
+ 'type',
+ 'explanation_entities',
+ 'is_anonymous',
+ 'close_date',
+ 'is_closed',
+ 'id',
+ 'explanation',
+ 'question',
+ 'correct_option_id',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ id: str, # pylint: disable=W0622
+ question: str,
+ options: List[PollOption],
+ total_voter_count: int,
+ is_closed: bool,
+ is_anonymous: bool,
+ type: str, # pylint: disable=W0622
+ allows_multiple_answers: bool,
+ correct_option_id: int = None,
+ explanation: str = None,
+ explanation_entities: List[MessageEntity] = None,
+ open_period: int = None,
+ close_date: datetime.datetime = None,
+ **_kwargs: Any,
+ ):
+ self.id = id # pylint: disable=C0103
+ self.question = question
+ self.options = options
+ self.total_voter_count = total_voter_count
+ self.is_closed = is_closed
+ self.is_anonymous = is_anonymous
+ self.type = type
+ self.allows_multiple_answers = allows_multiple_answers
+ self.correct_option_id = correct_option_id
+ self.explanation = explanation
+ self.explanation_entities = explanation_entities
+ self.open_period = open_period
+ self.close_date = close_date
+
+ self._id_attrs = (self.id,)
+
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Poll']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ data['options'] = [PollOption.de_json(option, bot) for option in data['options']]
+ data['explanation_entities'] = MessageEntity.de_list(data.get('explanation_entities'), bot)
+ data['close_date'] = from_timestamp(data.get('close_date'))
+
+ return cls(**data)
+
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
+
+ data['options'] = [x.to_dict() for x in self.options]
+ if self.explanation_entities:
+ data['explanation_entities'] = [e.to_dict() for e in self.explanation_entities]
+ data['close_date'] = to_timestamp(data.get('close_date'))
+
+ return data
+
+ def parse_explanation_entity(self, entity: MessageEntity) -> str:
+ """Returns the text from a given :class:`telegram.MessageEntity`.
+
+ Note:
+ This method is present because Telegram calculates the offset and length in
+ UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
+ (That is, you can't just slice ``Message.text`` with the offset and length.)
+
+ Args:
+ entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
+ be an entity that belongs to this message.
+
+ Returns:
+ :obj:`str`: The text of the given entity.
+
+ Raises:
+ RuntimeError: If the poll has no explanation.
+
+ """
+ if not self.explanation:
+ raise RuntimeError("This Poll has no 'explanation'.")
+
+ # Is it a narrow build, if so we don't need to convert
+ if sys.maxunicode == 0xFFFF:
+ return self.explanation[entity.offset : entity.offset + entity.length]
+ entity_text = self.explanation.encode('utf-16-le')
+ entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
+
+ return entity_text.decode('utf-16-le')
+
+ def parse_explanation_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]:
+ """
+ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
+ It contains entities from this polls explanation filtered by their ``type`` attribute as
+ the key, and the text that each entity belongs to as the value of the :obj:`dict`.
+
+ Note:
+ This method should always be used instead of the :attr:`explanation_entities`
+ attribute, since it calculates the correct substring from the message text based on
+ UTF-16 codepoints. See :attr:`parse_explanation_entity` for more info.
+
+ Args:
+ types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
+ ``type`` attribute of an entity is contained in this list, it will be returned.
+ Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
+
+ Returns:
+ Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
+ the text that belongs to them, calculated based on UTF-16 codepoints.
+
+ """
+ if types is None:
+ types = MessageEntity.ALL_TYPES
+
+ return {
+ entity: self.parse_explanation_entity(entity)
+ for entity in (self.explanation_entities or [])
+ if entity.type in types
+ }
+
+ REGULAR: ClassVar[str] = constants.POLL_REGULAR
+ """:const:`telegram.constants.POLL_REGULAR`"""
+ QUIZ: ClassVar[str] = constants.POLL_QUIZ
+ """:const:`telegram.constants.POLL_QUIZ`"""
+ MAX_QUESTION_LENGTH: ClassVar[int] = constants.MAX_POLL_QUESTION_LENGTH
+ """:const:`telegram.constants.MAX_POLL_QUESTION_LENGTH`"""
+ MAX_OPTION_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH
+ """:const:`telegram.constants.MAX_POLL_OPTION_LENGTH`"""
diff --git a/telegramer/include/telegram/proximityalerttriggered.py b/telegramer/include/telegram/proximityalerttriggered.py
new file mode 100644
index 0000000..df4cb55
--- /dev/null
+++ b/telegramer/include/telegram/proximityalerttriggered.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram Proximity Alert."""
+from typing import Any, Optional, TYPE_CHECKING
+
+from telegram import TelegramObject, User
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+
+class ProximityAlertTriggered(TelegramObject):
+ """
+ This object represents the content of a service message, sent whenever a user in the chat
+ triggers a proximity alert set by another user.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`traveler`, :attr:`watcher` and :attr:`distance` are equal.
+
+ Args:
+ traveler (:class:`telegram.User`): User that triggered the alert
+ watcher (:class:`telegram.User`): User that set the alert
+ distance (:obj:`int`): The distance between the users
+
+ Attributes:
+ traveler (:class:`telegram.User`): User that triggered the alert
+ watcher (:class:`telegram.User`): User that set the alert
+ distance (:obj:`int`): The distance between the users
+
+ """
+
+ __slots__ = ('traveler', 'distance', 'watcher', '_id_attrs')
+
+ def __init__(self, traveler: User, watcher: User, distance: int, **_kwargs: Any):
+ self.traveler = traveler
+ self.watcher = watcher
+ self.distance = distance
+
+ self._id_attrs = (self.traveler, self.watcher, self.distance)
+
+ @classmethod
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ProximityAlertTriggered']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ if not data:
+ return None
+
+ data['traveler'] = User.de_json(data.get('traveler'), bot)
+ data['watcher'] = User.de_json(data.get('watcher'), bot)
+
+ return cls(bot=bot, **data)
diff --git a/telegramer/include/telegram/py.typed b/telegramer/include/telegram/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/telegramer/include/telegram/replykeyboardmarkup.py b/telegramer/include/telegram/replykeyboardmarkup.py
index d27572a..cc6f985 100644
--- a/telegramer/include/telegram/replykeyboardmarkup.py
+++ b/telegramer/include/telegram/replykeyboardmarkup.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,18 +18,17 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ReplyKeyboardMarkup."""
-from telegram import ReplyMarkup
+from typing import Any, List, Union, Sequence
+
+from telegram import KeyboardButton, ReplyMarkup
+from telegram.utils.types import JSONDict
class ReplyKeyboardMarkup(ReplyMarkup):
"""This object represents a custom keyboard with reply options.
- Attributes:
- keyboard (List[List[:class:`telegram.KeyboardButton` | :obj:`str`]]): Array of button rows.
- resize_keyboard (:obj:`bool`): Optional. Requests clients to resize the keyboard.
- one_time_keyboard (:obj:`bool`): Optional. Requests clients to hide the keyboard as soon as
- it's been used.
- selective (:obj:`bool`): Optional. Show the keyboard to specific users only.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their the size of :attr:`keyboard` and all the buttons are equal.
Example:
A user requests to change the bot's language, bot replies to the request with a keyboard
@@ -40,48 +39,253 @@ class ReplyKeyboardMarkup(ReplyMarkup):
each represented by an Array of :class:`telegram.KeyboardButton` objects.
resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard vertically
for optimal fit (e.g., make the keyboard smaller if there are just two rows of
- buttons). Defaults to false, in which case the custom keyboard is always of the same
- height as the app's standard keyboard. Defaults to ``False``
+ buttons). Defaults to :obj:`False`, in which case the custom keyboard is always of the
+ same height as the app's standard keyboard.
one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as soon as
it's been used. The keyboard will still be available, but clients will automatically
display the usual letter-keyboard in the chat - the user can press a special button in
- the input field to see the custom keyboard again. Defaults to ``False``.
+ the input field to see the custom keyboard again. Defaults to :obj:`False`.
selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard to
specific users only. Targets:
- 1) users that are @mentioned in the text of the Message object
- 2) if the bot's message is a reply (has reply_to_message_id), sender of the original
- message.
+ 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the
+ :class:`telegram.Message` object.
+ 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
+ original message.
- Defaults to ``False``.
+ Defaults to :obj:`False`.
+
+ input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input
+ field when the keyboard is active; 1-64 characters.
+
+ .. versionadded:: 13.7
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ keyboard (List[List[:class:`telegram.KeyboardButton` | :obj:`str`]]): Array of button rows.
+ resize_keyboard (:obj:`bool`): Optional. Requests clients to resize the keyboard.
+ one_time_keyboard (:obj:`bool`): Optional. Requests clients to hide the keyboard as soon as
+ it's been used.
+ selective (:obj:`bool`): Optional. Show the keyboard to specific users only.
+ input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
+ field when the reply is active.
+
+ .. versionadded:: 13.7
+
"""
- def __init__(self,
- keyboard,
- resize_keyboard=False,
- one_time_keyboard=False,
- selective=False,
- **kwargs):
+ __slots__ = (
+ 'selective',
+ 'keyboard',
+ 'resize_keyboard',
+ 'one_time_keyboard',
+ 'input_field_placeholder',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ keyboard: Sequence[Sequence[Union[str, KeyboardButton]]],
+ resize_keyboard: bool = False,
+ one_time_keyboard: bool = False,
+ selective: bool = False,
+ input_field_placeholder: str = None,
+ **_kwargs: Any,
+ ):
# Required
- self.keyboard = keyboard
+ self.keyboard = []
+ for row in keyboard:
+ button_row = []
+ for button in row:
+ if isinstance(button, KeyboardButton):
+ button_row.append(button) # telegram.KeyboardButton
+ else:
+ button_row.append(KeyboardButton(button)) # str
+ self.keyboard.append(button_row)
+
# Optionals
self.resize_keyboard = bool(resize_keyboard)
self.one_time_keyboard = bool(one_time_keyboard)
self.selective = bool(selective)
+ self.input_field_placeholder = input_field_placeholder
+
+ self._id_attrs = (self.keyboard,)
- def to_dict(self):
- data = super(ReplyKeyboardMarkup, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
data['keyboard'] = []
for row in self.keyboard:
- r = []
- for button in row:
- if hasattr(button, 'to_dict'):
- r.append(button.to_dict()) # telegram.KeyboardButton
- else:
- r.append(button) # str
- data['keyboard'].append(r)
+ data['keyboard'].append([button.to_dict() for button in row])
return data
+
+ @classmethod
+ def from_button(
+ cls,
+ button: Union[KeyboardButton, str],
+ resize_keyboard: bool = False,
+ one_time_keyboard: bool = False,
+ selective: bool = False,
+ input_field_placeholder: str = None,
+ **kwargs: object,
+ ) -> 'ReplyKeyboardMarkup':
+ """Shortcut for::
+
+ ReplyKeyboardMarkup([[button]], **kwargs)
+
+ Return a ReplyKeyboardMarkup from a single KeyboardButton.
+
+ Args:
+ button (:class:`telegram.KeyboardButton` | :obj:`str`): The button to use in
+ the markup.
+ resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard
+ vertically for optimal fit (e.g., make the keyboard smaller if there are just two
+ rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is
+ always of the same height as the app's standard keyboard.
+ one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as
+ soon as it's been used. The keyboard will still be available, but clients will
+ automatically display the usual letter-keyboard in the chat - the user can press
+ a special button in the input field to see the custom keyboard again.
+ Defaults to :obj:`False`.
+ selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard
+ to specific users only. Targets:
+
+ 1) Users that are @mentioned in the text of the Message object.
+ 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
+ original message.
+
+ Defaults to :obj:`False`.
+
+ input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
+ field when the reply is active.
+
+ .. versionadded:: 13.7
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ """
+ return cls(
+ [[button]],
+ resize_keyboard=resize_keyboard,
+ one_time_keyboard=one_time_keyboard,
+ selective=selective,
+ input_field_placeholder=input_field_placeholder,
+ **kwargs,
+ )
+
+ @classmethod
+ def from_row(
+ cls,
+ button_row: List[Union[str, KeyboardButton]],
+ resize_keyboard: bool = False,
+ one_time_keyboard: bool = False,
+ selective: bool = False,
+ input_field_placeholder: str = None,
+ **kwargs: object,
+ ) -> 'ReplyKeyboardMarkup':
+ """Shortcut for::
+
+ ReplyKeyboardMarkup([button_row], **kwargs)
+
+ Return a ReplyKeyboardMarkup from a single row of KeyboardButtons.
+
+ Args:
+ button_row (List[:class:`telegram.KeyboardButton` | :obj:`str`]): The button to use in
+ the markup.
+ resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard
+ vertically for optimal fit (e.g., make the keyboard smaller if there are just two
+ rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is
+ always of the same height as the app's standard keyboard.
+ one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as
+ soon as it's been used. The keyboard will still be available, but clients will
+ automatically display the usual letter-keyboard in the chat - the user can press
+ a special button in the input field to see the custom keyboard again.
+ Defaults to :obj:`False`.
+ selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard
+ to specific users only. Targets:
+
+ 1) Users that are @mentioned in the text of the Message object.
+ 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
+ original message.
+
+ Defaults to :obj:`False`.
+
+ input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
+ field when the reply is active.
+
+ .. versionadded:: 13.7
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
+ """
+ return cls(
+ [button_row],
+ resize_keyboard=resize_keyboard,
+ one_time_keyboard=one_time_keyboard,
+ selective=selective,
+ input_field_placeholder=input_field_placeholder,
+ **kwargs,
+ )
+
+ @classmethod
+ def from_column(
+ cls,
+ button_column: List[Union[str, KeyboardButton]],
+ resize_keyboard: bool = False,
+ one_time_keyboard: bool = False,
+ selective: bool = False,
+ input_field_placeholder: str = None,
+ **kwargs: object,
+ ) -> 'ReplyKeyboardMarkup':
+ """Shortcut for::
+
+ ReplyKeyboardMarkup([[button] for button in button_column], **kwargs)
+
+ Return a ReplyKeyboardMarkup from a single column of KeyboardButtons.
+
+ Args:
+ button_column (List[:class:`telegram.KeyboardButton` | :obj:`str`]): The button to use
+ in the markup.
+ resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard
+ vertically for optimal fit (e.g., make the keyboard smaller if there are just two
+ rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is
+ always of the same height as the app's standard keyboard.
+ one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as
+ soon as it's been used. The keyboard will still be available, but clients will
+ automatically display the usual letter-keyboard in the chat - the user can press
+ a special button in the input field to see the custom keyboard again.
+ Defaults to :obj:`False`.
+ selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard
+ to specific users only. Targets:
+
+ 1) Users that are @mentioned in the text of the Message object.
+ 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
+ original message.
+
+ Defaults to :obj:`False`.
+
+ input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
+ field when the reply is active.
+
+ .. versionadded:: 13.7
+ **kwargs (:obj:`dict`): Arbitrary keyword arguments.
+
+ """
+ button_grid = [[button] for button in button_column]
+ return cls(
+ button_grid,
+ resize_keyboard=resize_keyboard,
+ one_time_keyboard=one_time_keyboard,
+ selective=selective,
+ input_field_placeholder=input_field_placeholder,
+ **kwargs,
+ )
+
+ def __hash__(self) -> int:
+ return hash(
+ (
+ tuple(tuple(button for button in row) for row in self.keyboard),
+ self.resize_keyboard,
+ self.one_time_keyboard,
+ self.selective,
+ )
+ )
diff --git a/telegramer/include/telegram/replykeyboardremove.py b/telegramer/include/telegram/replykeyboardremove.py
index 6d31bd4..2762141 100644
--- a/telegramer/include/telegram/replykeyboardremove.py
+++ b/telegramer/include/telegram/replykeyboardremove.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ReplyKeyboardRemove."""
+from typing import Any
+
from telegram import ReplyMarkup
@@ -27,29 +29,35 @@ class ReplyKeyboardRemove(ReplyMarkup):
until a new keyboard is sent by a bot. An exception is made for one-time keyboards that are
hidden immediately after the user presses a button (see :class:`telegram.ReplyKeyboardMarkup`).
- Attributes:
- remove_keyboard (:obj:`True`): Requests clients to remove the custom keyboard.
- selective (:obj:`bool`): Optional. Use this parameter if you want to remove the keyboard
- for specific users only.
-
Example:
A user votes in a poll, bot returns confirmation message in reply to the vote and removes
the keyboard for that user, while still showing the keyboard with poll options to users who
haven't voted yet.
+ Note:
+ User will not be able to summon this keyboard; if you want to hide the keyboard from
+ sight but keep it accessible, use :attr:`telegram.ReplyKeyboardMarkup.one_time_keyboard`.
+
Args:
selective (:obj:`bool`, optional): Use this parameter if you want to remove the keyboard
for specific users only. Targets:
- 1) users that are @mentioned in the text of the Message object
- 2) if the bot's message is a reply (has reply_to_message_id), sender of the original
+ 1) Users that are @mentioned in the text of the :class:`telegram.Message` object.
+ 2) If the bot's message is a reply (has `reply_to_message_id`), sender of the original
message.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ remove_keyboard (:obj:`True`): Requests clients to remove the custom keyboard.
+ selective (:obj:`bool`): Optional. Use this parameter if you want to remove the keyboard
+ for specific users only.
+
"""
- def __init__(self, selective=False, **kwargs):
+ __slots__ = ('selective', 'remove_keyboard')
+
+ def __init__(self, selective: bool = False, **_kwargs: Any):
# Required
self.remove_keyboard = True
# Optionals
diff --git a/telegramer/include/telegram/replymarkup.py b/telegramer/include/telegram/replymarkup.py
index 47928b7..081a73a 100644
--- a/telegramer/include/telegram/replymarkup.py
+++ b/telegramer/include/telegram/replymarkup.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -24,8 +24,10 @@
class ReplyMarkup(TelegramObject):
"""Base class for Telegram ReplyMarkup Objects.
- See :class:`telegram.ReplyKeyboardMarkup` and :class:`telegram.InlineKeyboardMarkup` for
+ See :class:`telegram.InlineKeyboardMarkup`, :class:`telegram.ReplyKeyboardMarkup`,
+ :class:`telegram.ReplyKeyboardRemove` and :class:`telegram.ForceReply` for
detailed use.
"""
- pass
+
+ __slots__ = ()
diff --git a/telegramer/include/telegram/update.py b/telegramer/include/telegram/update.py
index e69fd3a..7c7b210 100644
--- a/telegramer/include/telegram/update.py
+++ b/telegramer/include/telegram/update.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,35 +18,44 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Update."""
-from telegram import (Message, TelegramObject, InlineQuery, ChosenInlineResult,
- CallbackQuery, ShippingQuery, PreCheckoutQuery)
+from typing import TYPE_CHECKING, Any, Optional
+
+from telegram import (
+ CallbackQuery,
+ ChosenInlineResult,
+ InlineQuery,
+ Message,
+ Poll,
+ PreCheckoutQuery,
+ ShippingQuery,
+ TelegramObject,
+ ChatMemberUpdated,
+ constants,
+ ChatJoinRequest,
+)
+from telegram.poll import PollAnswer
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot, Chat, User # noqa
class Update(TelegramObject):
"""This object represents an incoming update.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`update_id` is equal.
+
Note:
At most one of the optional parameters can be present in any given update.
- Attributes:
- update_id (:obj:`int`): The update's unique identifier.
- message (:class:`telegram.Message`): Optional. New incoming message.
- edited_message (:class:`telegram.Message`): Optional. New version of a message.
- channel_post (:class:`telegram.Message`): Optional. New incoming channel post.
- edited_channel_post (:class:`telegram.Message`): Optional. New version of a channel post.
- inline_query (:class:`telegram.InlineQuery`): Optional. New incoming inline query.
- chosen_inline_result (:class:`telegram.ChosenInlineResult`): Optional. The result of an
- inline query that was chosen by a user.
- callback_query (:class:`telegram.CallbackQuery`): Optional. New incoming callback query.
- shipping_query (:class:`telegram.ShippingQuery`): Optional. New incoming shipping query.
- pre_checkout_query (:class:`telegram.PreCheckoutQuery`): Optional. New incoming
- pre-checkout query.
-
Args:
update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a
certain positive number and increase sequentially. This ID becomes especially handy if
you're using Webhooks, since it allows you to ignore repeated updates or to restore the
- correct update sequence, should they get out of order.
+ correct update sequence, should they get out of order. If there are no new updates for
+ at least a week, then identifier of the next update will be chosen randomly instead of
+ sequentially.
message (:class:`telegram.Message`, optional): New incoming message of any kind - text,
photo, sticker, etc.
edited_message (:class:`telegram.Message`, optional): New version of a message that is
@@ -62,23 +71,174 @@ class Update(TelegramObject):
shipping_query (:class:`telegram.ShippingQuery`, optional): New incoming shipping query.
Only for invoices with flexible price.
pre_checkout_query (:class:`telegram.PreCheckoutQuery`, optional): New incoming
- pre-checkout query. Contains full information about checkout
+ pre-checkout query. Contains full information about checkout.
+ poll (:class:`telegram.Poll`, optional): New poll state. Bots receive only updates about
+ stopped polls and polls, which are sent by the bot.
+ poll_answer (:class:`telegram.PollAnswer`, optional): A user changed their answer
+ in a non-anonymous poll. Bots receive new votes only in polls that were sent
+ by the bot itself.
+ my_chat_member (:class:`telegram.ChatMemberUpdated`, optional): The bot's chat member
+ status was updated in a chat. For private chats, this update is received only when the
+ bot is blocked or unblocked by the user.
+
+ .. versionadded:: 13.4
+ chat_member (:class:`telegram.ChatMemberUpdated`, optional): A chat member's status was
+ updated in a chat. The bot must be an administrator in the chat and must explicitly
+ specify ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these
+ updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`,
+ :meth:`telegram.ext.Updater.start_polling` and
+ :meth:`telegram.ext.Updater.start_webhook`).
+
+ .. versionadded:: 13.4
+ chat_join_request (:class:`telegram.ChatJoinRequest`, optional): A request to join the
+ chat has been sent. The bot must have the
+ :attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat to
+ receive these updates.
+
+ .. versionadded:: 13.8
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
+ Attributes:
+ update_id (:obj:`int`): The update's unique identifier.
+ message (:class:`telegram.Message`): Optional. New incoming message.
+ edited_message (:class:`telegram.Message`): Optional. New version of a message.
+ channel_post (:class:`telegram.Message`): Optional. New incoming channel post.
+ edited_channel_post (:class:`telegram.Message`): Optional. New version of a channel post.
+ inline_query (:class:`telegram.InlineQuery`): Optional. New incoming inline query.
+ chosen_inline_result (:class:`telegram.ChosenInlineResult`): Optional. The result of an
+ inline query that was chosen by a user.
+ callback_query (:class:`telegram.CallbackQuery`): Optional. New incoming callback query.
+ shipping_query (:class:`telegram.ShippingQuery`): Optional. New incoming shipping query.
+ pre_checkout_query (:class:`telegram.PreCheckoutQuery`): Optional. New incoming
+ pre-checkout query.
+ poll (:class:`telegram.Poll`): Optional. New poll state. Bots receive only updates
+ about stopped polls and polls, which are sent by the bot.
+ poll_answer (:class:`telegram.PollAnswer`): Optional. A user changed their answer
+ in a non-anonymous poll. Bots receive new votes only in polls that were sent
+ by the bot itself.
+ my_chat_member (:class:`telegram.ChatMemberUpdated`): Optional. The bot's chat member
+ status was updated in a chat. For private chats, this update is received only when the
+ bot is blocked or unblocked by the user.
+
+ .. versionadded:: 13.4
+ chat_member (:class:`telegram.ChatMemberUpdated`): Optional. A chat member's status was
+ updated in a chat. The bot must be an administrator in the chat and must explicitly
+ specify ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these
+ updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`,
+ :meth:`telegram.ext.Updater.start_polling` and
+ :meth:`telegram.ext.Updater.start_webhook`).
+
+ .. versionadded:: 13.4
+ chat_join_request (:class:`telegram.ChatJoinRequest`): Optional. A request to join the
+ chat has been sent. The bot must have the ``'can_invite_users'`` administrator
+ right in the chat to receive these updates.
+
+ .. versionadded:: 13.8
+
"""
- def __init__(self,
- update_id,
- message=None,
- edited_message=None,
- channel_post=None,
- edited_channel_post=None,
- inline_query=None,
- chosen_inline_result=None,
- callback_query=None,
- shipping_query=None,
- pre_checkout_query=None,
- **kwargs):
+ __slots__ = (
+ 'callback_query',
+ 'chosen_inline_result',
+ 'pre_checkout_query',
+ 'inline_query',
+ 'update_id',
+ 'message',
+ 'shipping_query',
+ 'poll',
+ 'poll_answer',
+ 'channel_post',
+ 'edited_channel_post',
+ 'edited_message',
+ '_effective_user',
+ '_effective_chat',
+ '_effective_message',
+ 'my_chat_member',
+ 'chat_member',
+ 'chat_join_request',
+ '_id_attrs',
+ )
+
+ MESSAGE = constants.UPDATE_MESSAGE
+ """:const:`telegram.constants.UPDATE_MESSAGE`
+
+ .. versionadded:: 13.5"""
+ EDITED_MESSAGE = constants.UPDATE_EDITED_MESSAGE
+ """:const:`telegram.constants.UPDATE_EDITED_MESSAGE`
+
+ .. versionadded:: 13.5"""
+ CHANNEL_POST = constants.UPDATE_CHANNEL_POST
+ """:const:`telegram.constants.UPDATE_CHANNEL_POST`
+
+ .. versionadded:: 13.5"""
+ EDITED_CHANNEL_POST = constants.UPDATE_EDITED_CHANNEL_POST
+ """:const:`telegram.constants.UPDATE_EDITED_CHANNEL_POST`
+
+ .. versionadded:: 13.5"""
+ INLINE_QUERY = constants.UPDATE_INLINE_QUERY
+ """:const:`telegram.constants.UPDATE_INLINE_QUERY`
+
+ .. versionadded:: 13.5"""
+ CHOSEN_INLINE_RESULT = constants.UPDATE_CHOSEN_INLINE_RESULT
+ """:const:`telegram.constants.UPDATE_CHOSEN_INLINE_RESULT`
+
+ .. versionadded:: 13.5"""
+ CALLBACK_QUERY = constants.UPDATE_CALLBACK_QUERY
+ """:const:`telegram.constants.UPDATE_CALLBACK_QUERY`
+
+ .. versionadded:: 13.5"""
+ SHIPPING_QUERY = constants.UPDATE_SHIPPING_QUERY
+ """:const:`telegram.constants.UPDATE_SHIPPING_QUERY`
+
+ .. versionadded:: 13.5"""
+ PRE_CHECKOUT_QUERY = constants.UPDATE_PRE_CHECKOUT_QUERY
+ """:const:`telegram.constants.UPDATE_PRE_CHECKOUT_QUERY`
+
+ .. versionadded:: 13.5"""
+ POLL = constants.UPDATE_POLL
+ """:const:`telegram.constants.UPDATE_POLL`
+
+ .. versionadded:: 13.5"""
+ POLL_ANSWER = constants.UPDATE_POLL_ANSWER
+ """:const:`telegram.constants.UPDATE_POLL_ANSWER`
+
+ .. versionadded:: 13.5"""
+ MY_CHAT_MEMBER = constants.UPDATE_MY_CHAT_MEMBER
+ """:const:`telegram.constants.UPDATE_MY_CHAT_MEMBER`
+
+ .. versionadded:: 13.5"""
+ CHAT_MEMBER = constants.UPDATE_CHAT_MEMBER
+ """:const:`telegram.constants.UPDATE_CHAT_MEMBER`
+
+ .. versionadded:: 13.5"""
+ CHAT_JOIN_REQUEST = constants.UPDATE_CHAT_JOIN_REQUEST
+ """:const:`telegram.constants.UPDATE_CHAT_JOIN_REQUEST`
+
+ .. versionadded:: 13.8"""
+ ALL_TYPES = constants.UPDATE_ALL_TYPES
+ """:const:`telegram.constants.UPDATE_ALL_TYPES`
+
+ .. versionadded:: 13.5"""
+
+ def __init__(
+ self,
+ update_id: int,
+ message: Message = None,
+ edited_message: Message = None,
+ channel_post: Message = None,
+ edited_channel_post: Message = None,
+ inline_query: InlineQuery = None,
+ chosen_inline_result: ChosenInlineResult = None,
+ callback_query: CallbackQuery = None,
+ shipping_query: ShippingQuery = None,
+ pre_checkout_query: PreCheckoutQuery = None,
+ poll: Poll = None,
+ poll_answer: PollAnswer = None,
+ my_chat_member: ChatMemberUpdated = None,
+ chat_member: ChatMemberUpdated = None,
+ chat_join_request: ChatJoinRequest = None,
+ **_kwargs: Any,
+ ):
# Required
self.update_id = int(update_id)
# Optionals
@@ -91,18 +251,23 @@ def __init__(self,
self.pre_checkout_query = pre_checkout_query
self.channel_post = channel_post
self.edited_channel_post = edited_channel_post
+ self.poll = poll
+ self.poll_answer = poll_answer
+ self.my_chat_member = my_chat_member
+ self.chat_member = chat_member
+ self.chat_join_request = chat_join_request
- self._effective_user = None
- self._effective_chat = None
- self._effective_message = None
+ self._effective_user: Optional['User'] = None
+ self._effective_chat: Optional['Chat'] = None
+ self._effective_message: Optional[Message] = None
self._id_attrs = (self.update_id,)
@property
- def effective_user(self):
+ def effective_user(self) -> Optional['User']:
"""
:class:`telegram.User`: The user that sent this update, no matter what kind of update this
- is. Will be ``None`` for :attr:`channel_post`.
+ is. Will be :obj:`None` for :attr:`channel_post` and :attr:`poll`.
"""
if self._effective_user:
@@ -131,16 +296,29 @@ def effective_user(self):
elif self.pre_checkout_query:
user = self.pre_checkout_query.from_user
+ elif self.poll_answer:
+ user = self.poll_answer.user
+
+ elif self.my_chat_member:
+ user = self.my_chat_member.from_user
+
+ elif self.chat_member:
+ user = self.chat_member.from_user
+
+ elif self.chat_join_request:
+ user = self.chat_join_request.from_user
+
self._effective_user = user
return user
@property
- def effective_chat(self):
+ def effective_chat(self) -> Optional['Chat']:
"""
:class:`telegram.Chat`: The chat that this update was sent in, no matter what kind of
- update this is. Will be ``None`` for :attr:`inline_query`,
+ update this is. Will be :obj:`None` for :attr:`inline_query`,
:attr:`chosen_inline_result`, :attr:`callback_query` from inline messages,
- :attr:`shipping_query` and :attr:`pre_checkout_query`.
+ :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll` and
+ :attr:`poll_answer`.
"""
if self._effective_chat:
@@ -163,16 +341,28 @@ def effective_chat(self):
elif self.edited_channel_post:
chat = self.edited_channel_post.chat
+ elif self.my_chat_member:
+ chat = self.my_chat_member.chat
+
+ elif self.chat_member:
+ chat = self.chat_member.chat
+
+ elif self.chat_join_request:
+ chat = self.chat_join_request.chat
+
self._effective_chat = chat
return chat
@property
- def effective_message(self):
+ def effective_message(self) -> Optional[Message]:
"""
:class:`telegram.Message`: The message included in this update, no matter what kind of
- update this is. Will be ``None`` for :attr:`inline_query`,
+ update this is. Will be :obj:`None` for :attr:`inline_query`,
:attr:`chosen_inline_result`, :attr:`callback_query` from inline messages,
- :attr:`shipping_query` and :attr:`pre_checkout_query`.
+ :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`,
+ :attr:`poll_answer`, :attr:`my_chat_member`, :attr:`chat_member` as well as
+ :attr:`chat_join_request` in case the bot is missing the
+ :attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat.
"""
if self._effective_message:
@@ -199,21 +389,28 @@ def effective_message(self):
return message
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Update']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(Update, cls).de_json(data, bot)
-
data['message'] = Message.de_json(data.get('message'), bot)
data['edited_message'] = Message.de_json(data.get('edited_message'), bot)
data['inline_query'] = InlineQuery.de_json(data.get('inline_query'), bot)
data['chosen_inline_result'] = ChosenInlineResult.de_json(
- data.get('chosen_inline_result'), bot)
+ data.get('chosen_inline_result'), bot
+ )
data['callback_query'] = CallbackQuery.de_json(data.get('callback_query'), bot)
data['shipping_query'] = ShippingQuery.de_json(data.get('shipping_query'), bot)
data['pre_checkout_query'] = PreCheckoutQuery.de_json(data.get('pre_checkout_query'), bot)
data['channel_post'] = Message.de_json(data.get('channel_post'), bot)
data['edited_channel_post'] = Message.de_json(data.get('edited_channel_post'), bot)
+ data['poll'] = Poll.de_json(data.get('poll'), bot)
+ data['poll_answer'] = PollAnswer.de_json(data.get('poll_answer'), bot)
+ data['my_chat_member'] = ChatMemberUpdated.de_json(data.get('my_chat_member'), bot)
+ data['chat_member'] = ChatMemberUpdated.de_json(data.get('chat_member'), bot)
+ data['chat_join_request'] = ChatJoinRequest.de_json(data.get('chat_join_request'), bot)
return cls(**data)
diff --git a/telegramer/include/telegram/user.py b/telegramer/include/telegram/user.py
index e140cfc..0a50895 100644
--- a/telegramer/include/telegram/user.py
+++ b/telegramer/include/telegram/user.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python
-# pylint: disable=C0103,W0622
+# pylint: disable=W0622
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,127 +18,210 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram User."""
-
-from telegram import TelegramObject
-from telegram.utils.helpers import mention_html as util_mention_html
+from datetime import datetime
+from typing import TYPE_CHECKING, Any, List, Optional, Union, Tuple
+
+from telegram import TelegramObject, constants
+from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton
+from telegram.utils.helpers import (
+ mention_html as util_mention_html,
+ DEFAULT_NONE,
+ DEFAULT_20,
+)
from telegram.utils.helpers import mention_markdown as util_mention_markdown
+from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput
+
+if TYPE_CHECKING:
+ from telegram import (
+ Bot,
+ Message,
+ UserProfilePhotos,
+ MessageId,
+ InputMediaAudio,
+ InputMediaDocument,
+ InputMediaPhoto,
+ InputMediaVideo,
+ MessageEntity,
+ ReplyMarkup,
+ PhotoSize,
+ Audio,
+ Contact,
+ Document,
+ InlineKeyboardMarkup,
+ LabeledPrice,
+ Location,
+ Animation,
+ Sticker,
+ Video,
+ Venue,
+ VideoNote,
+ Voice,
+ )
class User(TelegramObject):
"""This object represents a Telegram user or bot.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`id` is equal.
+
+ Args:
+ id (:obj:`int`): Unique identifier for this user or bot.
+ is_bot (:obj:`bool`): :obj:`True`, if this user is a bot.
+ first_name (:obj:`str`): User's or bots first name.
+ last_name (:obj:`str`, optional): User's or bots last name.
+ username (:obj:`str`, optional): User's or bots username.
+ language_code (:obj:`str`, optional): IETF language tag of the user's language.
+ can_join_groups (:obj:`str`, optional): :obj:`True`, if the bot can be invited to groups.
+ Returned only in :attr:`telegram.Bot.get_me` requests.
+ can_read_all_group_messages (:obj:`str`, optional): :obj:`True`, if privacy mode is
+ disabled for the bot. Returned only in :attr:`telegram.Bot.get_me` requests.
+ supports_inline_queries (:obj:`str`, optional): :obj:`True`, if the bot supports inline
+ queries. Returned only in :attr:`telegram.Bot.get_me` requests.
+ bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
+
Attributes:
id (:obj:`int`): Unique identifier for this user or bot.
- is_bot (:obj:`bool`): True, if this user is a bot
+ is_bot (:obj:`bool`): :obj:`True`, if this user is a bot.
first_name (:obj:`str`): User's or bot's first name.
last_name (:obj:`str`): Optional. User's or bot's last name.
username (:obj:`str`): Optional. User's or bot's username.
language_code (:obj:`str`): Optional. IETF language tag of the user's language.
+ can_join_groups (:obj:`str`): Optional. :obj:`True`, if the bot can be invited to groups.
+ Returned only in :attr:`telegram.Bot.get_me` requests.
+ can_read_all_group_messages (:obj:`str`): Optional. :obj:`True`, if privacy mode is
+ disabled for the bot. Returned only in :attr:`telegram.Bot.get_me` requests.
+ supports_inline_queries (:obj:`str`): Optional. :obj:`True`, if the bot supports inline
+ queries. Returned only in :attr:`telegram.Bot.get_me` requests.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
- Args:
- id (:obj:`int`): Unique identifier for this user or bot.
- is_bot (:obj:`bool`): True, if this user is a bot
- first_name (:obj:`str`): User's or bot's first name.
- last_name (:obj:`str`, optional): User's or bot's last name.
- username (:obj:`str`, optional): User's or bot's username.
- language_code (:obj:`str`, optional): IETF language tag of the user's language.
- bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
-
"""
- def __init__(self,
- id,
- first_name,
- is_bot,
- last_name=None,
- username=None,
- language_code=None,
- bot=None,
- **kwargs):
+ __slots__ = (
+ 'is_bot',
+ 'can_read_all_group_messages',
+ 'username',
+ 'first_name',
+ 'last_name',
+ 'can_join_groups',
+ 'supports_inline_queries',
+ 'id',
+ 'bot',
+ 'language_code',
+ '_id_attrs',
+ )
+
+ def __init__(
+ self,
+ id: int,
+ first_name: str,
+ is_bot: bool,
+ last_name: str = None,
+ username: str = None,
+ language_code: str = None,
+ can_join_groups: bool = None,
+ can_read_all_group_messages: bool = None,
+ supports_inline_queries: bool = None,
+ bot: 'Bot' = None,
+ **_kwargs: Any,
+ ):
# Required
- self.id = int(id)
+ self.id = int(id) # pylint: disable=C0103
self.first_name = first_name
self.is_bot = is_bot
# Optionals
self.last_name = last_name
self.username = username
self.language_code = language_code
-
+ self.can_join_groups = can_join_groups
+ self.can_read_all_group_messages = can_read_all_group_messages
+ self.supports_inline_queries = supports_inline_queries
self.bot = bot
self._id_attrs = (self.id,)
@property
- def name(self):
+ def name(self) -> str:
""":obj:`str`: Convenience property. If available, returns the user's :attr:`username`
- prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`."""
+ prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`.
+ """
if self.username:
- return '@{}'.format(self.username)
+ return f'@{self.username}'
return self.full_name
@property
- def full_name(self):
+ def full_name(self) -> str:
""":obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if
- available) :attr:`last_name`."""
-
+ available) :attr:`last_name`.
+ """
if self.last_name:
- return u'{} {}'.format(self.first_name, self.last_name)
+ return f'{self.first_name} {self.last_name}'
return self.first_name
@property
- def link(self):
+ def link(self) -> Optional[str]:
""":obj:`str`: Convenience property. If :attr:`username` is available, returns a t.me link
- of the user."""
-
+ of the user.
+ """
if self.username:
- return "https://t.me/{}".format(self.username)
+ return f"https://t.me/{self.username}"
return None
- @classmethod
- def de_json(cls, data, bot):
- if not data:
- return None
-
- data = super(User, cls).de_json(data, bot)
-
- return cls(bot=bot, **data)
-
- def get_profile_photos(self, *args, **kwargs):
+ def get_profile_photos(
+ self,
+ offset: int = None,
+ limit: int = 100,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> Optional['UserProfilePhotos']:
"""
Shortcut for::
- bot.get_user_profile_photos(update.message.from_user.id, *args, **kwargs)
+ bot.get_user_profile_photos(update.effective_user.id, *args, **kwargs)
- """
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.get_user_profile_photos`.
- return self.bot.get_user_profile_photos(self.id, *args, **kwargs)
+ """
+ return self.bot.get_user_profile_photos(
+ user_id=self.id,
+ offset=offset,
+ limit=limit,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def mention_markdown(self, name: str = None) -> str:
+ """
+ Note:
+ :attr:`telegram.ParseMode.MARKDOWN` is a legacy mode, retained by Telegram for
+ backward compatibility. You should use :meth:`mention_markdown_v2` instead.
- @classmethod
- def de_list(cls, data, bot):
- if not data:
- return []
+ Args:
+ name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`.
- users = list()
- for user in data:
- users.append(cls.de_json(user, bot))
+ Returns:
+ :obj:`str`: The inline mention for the user as markdown (version 1).
- return users
+ """
+ if name:
+ return util_mention_markdown(self.id, name)
+ return util_mention_markdown(self.id, self.full_name)
- def mention_markdown(self, name=None):
+ def mention_markdown_v2(self, name: str = None) -> str:
"""
Args:
name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`.
Returns:
- :obj:`str`: The inline mention for the user as markdown.
+ :obj:`str`: The inline mention for the user as markdown (version 2).
"""
if name:
- return util_mention_markdown(self.id, name)
- return util_mention_markdown(self.id, self.full_name)
+ return util_mention_markdown(self.id, name, version=2)
+ return util_mention_markdown(self.id, self.full_name, version=2)
- def mention_html(self, name=None):
+ def mention_html(self, name: str = None) -> str:
"""
Args:
name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`.
@@ -151,119 +234,1010 @@ def mention_html(self, name=None):
return util_mention_html(self.id, name)
return util_mention_html(self.id, self.full_name)
- def send_message(self, *args, **kwargs):
+ def mention_button(self, name: str = None) -> InlineKeyboardButton:
+ """
+ Shortcut for::
+
+ InlineKeyboardButton(text=name, url=f"tg://user?id={update.effective_user.id}")
+
+ .. versionadded:: 13.9
+
+ Args:
+ name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`.
+
+ Returns:
+ :class:`telegram.InlineKeyboardButton`: InlineButton with url set to the user mention
+ """
+ return InlineKeyboardButton(text=name or self.full_name, url=f"tg://user?id={self.id}")
+
+ def pin_message(
+ self,
+ message_id: int,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.pin_chat_message(chat_id=update.effective_user.id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.pin_chat_message`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.pin_chat_message(
+ chat_id=self.id,
+ message_id=message_id,
+ disable_notification=disable_notification,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def unpin_message(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ message_id: int = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.unpin_chat_message(chat_id=update.effective_user.id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.unpin_chat_message`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.unpin_chat_message(
+ chat_id=self.id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ message_id=message_id,
+ )
+
+ def unpin_all_messages(
+ self,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.unpin_all_chat_messages(chat_id=update.effective_user.id,
+ *args,
+ **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.unpin_all_chat_messages`.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.unpin_all_chat_messages(
+ chat_id=self.id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ def send_message(
+ self,
+ text: str,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_message(update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_message(
+ chat_id=self.id,
+ text=text,
+ parse_mode=parse_mode,
+ disable_web_page_preview=disable_web_page_preview,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ entities=entities,
+ protect_content=protect_content,
+ )
+
+ def send_photo(
+ self,
+ photo: Union[FileInput, 'PhotoSize'],
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_message(User.id, *args, **kwargs)
+ bot.send_photo(update.effective_user.id, *args, **kwargs)
- Where User is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_message(self.id, *args, **kwargs)
+ return self.bot.send_photo(
+ chat_id=self.id,
+ photo=photo,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_media_group(
+ self,
+ media: List[
+ Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo']
+ ],
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> List['Message']:
+ """Shortcut for::
+
+ bot.send_media_group(update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`.
- def send_photo(self, *args, **kwargs):
+ Returns:
+ List[:class:`telegram.Message`:] On success, instance representing the message posted.
+
+ """
+ return self.bot.send_media_group(
+ chat_id=self.id,
+ media=media,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_audio(
+ self,
+ audio: Union[FileInput, 'Audio'],
+ duration: int = None,
+ performer: str = None,
+ title: str = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_photo(User.id, *args, **kwargs)
+ bot.send_audio(update.effective_user.id, *args, **kwargs)
- Where User is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_photo(self.id, *args, **kwargs)
+ return self.bot.send_audio(
+ chat_id=self.id,
+ audio=audio,
+ duration=duration,
+ performer=performer,
+ title=title,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_chat_action(
+ self,
+ action: str,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.send_chat_action(update.effective_user.id, *args, **kwargs)
- def send_audio(self, *args, **kwargs):
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`.
+
+ Returns:
+ :obj:`True`: On success.
+
+ """
+ return self.bot.send_chat_action(
+ chat_id=self.id,
+ action=action,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ send_action = send_chat_action
+ """Alias for :attr:`send_chat_action`"""
+
+ def send_contact(
+ self,
+ phone_number: str = None,
+ first_name: str = None,
+ last_name: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ contact: 'Contact' = None,
+ vcard: str = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_contact(update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_contact(
+ chat_id=self.id,
+ phone_number=phone_number,
+ first_name=first_name,
+ last_name=last_name,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ contact=contact,
+ vcard=vcard,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_dice(
+ self,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ emoji: str = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_dice(update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_dice(
+ chat_id=self.id,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ emoji=emoji,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_document(
+ self,
+ document: Union[FileInput, 'Document'],
+ filename: str = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ disable_content_type_detection: bool = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_audio(User.id, *args, **kwargs)
+ bot.send_document(update.effective_user.id, *args, **kwargs)
- Where User is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_audio(self.id, *args, **kwargs)
+ return self.bot.send_document(
+ chat_id=self.id,
+ document=document,
+ filename=filename,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ disable_content_type_detection=disable_content_type_detection,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ protect_content=protect_content,
+ )
+
+ def send_game(
+ self,
+ game_short_name: str,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_game(update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
- def send_document(self, *args, **kwargs):
+ """
+ return self.bot.send_game(
+ chat_id=self.id,
+ game_short_name=game_short_name,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_invoice(
+ self,
+ title: str,
+ description: str,
+ payload: str,
+ provider_token: str,
+ currency: str,
+ prices: List['LabeledPrice'],
+ start_parameter: str = None,
+ photo_url: str = None,
+ photo_size: int = None,
+ photo_width: int = None,
+ photo_height: int = None,
+ need_name: bool = None,
+ need_phone_number: bool = None,
+ need_email: bool = None,
+ need_shipping_address: bool = None,
+ is_flexible: bool = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'InlineKeyboardMarkup' = None,
+ provider_data: Union[str, object] = None,
+ send_phone_number_to_provider: bool = None,
+ send_email_to_provider: bool = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ max_tip_amount: int = None,
+ suggested_tip_amounts: List[int] = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_document(User.id, *args, **kwargs)
+ bot.send_invoice(update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`.
- Where User is the current instance.
+ Warning:
+ As of API 5.2 :attr:`start_parameter` is an optional argument and therefore the order
+ of the arguments had to be changed. Use keyword arguments to make sure that the
+ arguments are passed correctly.
+
+ .. versionchanged:: 13.5
+ As of Bot API 5.2, the parameter :attr:`start_parameter` is optional.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_document(self.id, *args, **kwargs)
+ return self.bot.send_invoice(
+ chat_id=self.id,
+ title=title,
+ description=description,
+ payload=payload,
+ provider_token=provider_token,
+ currency=currency,
+ prices=prices,
+ start_parameter=start_parameter,
+ photo_url=photo_url,
+ photo_size=photo_size,
+ photo_width=photo_width,
+ photo_height=photo_height,
+ need_name=need_name,
+ need_phone_number=need_phone_number,
+ need_email=need_email,
+ need_shipping_address=need_shipping_address,
+ is_flexible=is_flexible,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ provider_data=provider_data,
+ send_phone_number_to_provider=send_phone_number_to_provider,
+ send_email_to_provider=send_email_to_provider,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ max_tip_amount=max_tip_amount,
+ suggested_tip_amounts=suggested_tip_amounts,
+ protect_content=protect_content,
+ )
+
+ def send_location(
+ self,
+ latitude: float = None,
+ longitude: float = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ location: 'Location' = None,
+ live_period: int = None,
+ api_kwargs: JSONDict = None,
+ horizontal_accuracy: float = None,
+ heading: int = None,
+ proximity_alert_radius: int = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
- def send_animation(self, *args, **kwargs):
+ bot.send_location(update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_location(
+ chat_id=self.id,
+ latitude=latitude,
+ longitude=longitude,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ location=location,
+ live_period=live_period,
+ api_kwargs=api_kwargs,
+ horizontal_accuracy=horizontal_accuracy,
+ heading=heading,
+ proximity_alert_radius=proximity_alert_radius,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_animation(
+ self,
+ animation: Union[FileInput, 'Animation'],
+ duration: int = None,
+ width: int = None,
+ height: int = None,
+ thumb: FileInput = None,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_animation(User.id, *args, **kwargs)
+ bot.send_animation(update.effective_user.id, *args, **kwargs)
- Where User is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_animation(self.id, *args, **kwargs)
+ return self.bot.send_animation(
+ chat_id=self.id,
+ animation=animation,
+ duration=duration,
+ width=width,
+ height=height,
+ thumb=thumb,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_sticker(
+ self,
+ sticker: Union[FileInput, 'Sticker'],
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_sticker(update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
- def send_sticker(self, *args, **kwargs):
+ """
+ return self.bot.send_sticker(
+ chat_id=self.id,
+ sticker=sticker,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_video(
+ self,
+ video: Union[FileInput, 'Video'],
+ duration: int = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ width: int = None,
+ height: int = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ supports_streaming: bool = None,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_sticker(User.id, *args, **kwargs)
+ bot.send_video(update.effective_user.id, *args, **kwargs)
- Where User is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_sticker(self.id, *args, **kwargs)
+ return self.bot.send_video(
+ chat_id=self.id,
+ video=video,
+ duration=duration,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ width=width,
+ height=height,
+ parse_mode=parse_mode,
+ supports_streaming=supports_streaming,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_venue(
+ self,
+ latitude: float = None,
+ longitude: float = None,
+ title: str = None,
+ address: str = None,
+ foursquare_id: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ venue: 'Venue' = None,
+ foursquare_type: str = None,
+ api_kwargs: JSONDict = None,
+ google_place_id: str = None,
+ google_place_type: str = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_venue(update.effective_user.id, *args, **kwargs)
- def send_video(self, *args, **kwargs):
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.send_venue(
+ chat_id=self.id,
+ latitude=latitude,
+ longitude=longitude,
+ title=title,
+ address=address,
+ foursquare_id=foursquare_id,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ venue=venue,
+ foursquare_type=foursquare_type,
+ api_kwargs=api_kwargs,
+ google_place_id=google_place_id,
+ google_place_type=google_place_type,
+ allow_sending_without_reply=allow_sending_without_reply,
+ protect_content=protect_content,
+ )
+
+ def send_video_note(
+ self,
+ video_note: Union[FileInput, 'VideoNote'],
+ duration: int = None,
+ length: int = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ thumb: FileInput = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_video(User.id, *args, **kwargs)
+ bot.send_video_note(update.effective_user.id, *args, **kwargs)
- Where User is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_video(self.id, *args, **kwargs)
+ return self.bot.send_video_note(
+ chat_id=self.id,
+ video_note=video_note,
+ duration=duration,
+ length=length,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ thumb=thumb,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_voice(
+ self,
+ voice: Union[FileInput, 'Voice'],
+ duration: int = None,
+ caption: str = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: DVInput[float] = DEFAULT_20,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ filename: str = None,
+ protect_content: bool = None,
+ ) -> 'Message':
+ """Shortcut for::
+
+ bot.send_voice(update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`.
+
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
- def send_video_note(self, *args, **kwargs):
+ """
+ return self.bot.send_voice(
+ chat_id=self.id,
+ voice=voice,
+ duration=duration,
+ caption=caption,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ parse_mode=parse_mode,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ caption_entities=caption_entities,
+ filename=filename,
+ protect_content=protect_content,
+ )
+
+ def send_poll(
+ self,
+ question: str,
+ options: List[str],
+ is_anonymous: bool = True,
+ # We use constant.POLL_REGULAR instead of Poll.REGULAR here to avoid circular imports
+ type: str = constants.POLL_REGULAR, # pylint: disable=W0622
+ allows_multiple_answers: bool = False,
+ correct_option_id: int = None,
+ is_closed: bool = None,
+ disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ explanation: str = None,
+ explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
+ open_period: int = None,
+ close_date: Union[int, datetime] = None,
+ api_kwargs: JSONDict = None,
+ allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
+ explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
+ protect_content: bool = None,
+ ) -> 'Message':
"""Shortcut for::
- bot.send_video_note(User.id, *args, **kwargs)
+ bot.send_poll(update.effective_user.id, *args, **kwargs)
- Where User is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_video_note(self.id, *args, **kwargs)
+ return self.bot.send_poll(
+ chat_id=self.id,
+ question=question,
+ options=options,
+ is_anonymous=is_anonymous,
+ type=type, # pylint=pylint,
+ allows_multiple_answers=allows_multiple_answers,
+ correct_option_id=correct_option_id,
+ is_closed=is_closed,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ explanation=explanation,
+ explanation_parse_mode=explanation_parse_mode,
+ open_period=open_period,
+ close_date=close_date,
+ api_kwargs=api_kwargs,
+ allow_sending_without_reply=allow_sending_without_reply,
+ explanation_entities=explanation_entities,
+ protect_content=protect_content,
+ )
+
+ def send_copy(
+ self,
+ from_chat_id: Union[str, int],
+ message_id: int,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> 'MessageId':
+ """Shortcut for::
+
+ bot.copy_message(chat_id=update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`.
- def send_voice(self, *args, **kwargs):
+ Returns:
+ :class:`telegram.Message`: On success, instance representing the message posted.
+
+ """
+ return self.bot.copy_message(
+ chat_id=self.id,
+ from_chat_id=from_chat_id,
+ message_id=message_id,
+ caption=caption,
+ parse_mode=parse_mode,
+ caption_entities=caption_entities,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ allow_sending_without_reply=allow_sending_without_reply,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ def copy_message(
+ self,
+ chat_id: Union[int, str],
+ message_id: int,
+ caption: str = None,
+ parse_mode: ODVInput[str] = DEFAULT_NONE,
+ caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
+ disable_notification: DVInput[bool] = DEFAULT_NONE,
+ reply_to_message_id: int = None,
+ allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
+ reply_markup: 'ReplyMarkup' = None,
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ protect_content: bool = None,
+ ) -> 'MessageId':
"""Shortcut for::
- bot.send_voice(User.id, *args, **kwargs)
+ bot.copy_message(from_chat_id=update.effective_user.id, *args, **kwargs)
- Where User is the current instance.
+ For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
- return self.bot.send_voice(self.id, *args, **kwargs)
+ return self.bot.copy_message(
+ from_chat_id=self.id,
+ chat_id=chat_id,
+ message_id=message_id,
+ caption=caption,
+ parse_mode=parse_mode,
+ caption_entities=caption_entities,
+ disable_notification=disable_notification,
+ reply_to_message_id=reply_to_message_id,
+ allow_sending_without_reply=allow_sending_without_reply,
+ reply_markup=reply_markup,
+ timeout=timeout,
+ api_kwargs=api_kwargs,
+ protect_content=protect_content,
+ )
+
+ def approve_join_request(
+ self,
+ chat_id: Union[int, str],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.approve_chat_join_request(user_id=update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.approve_chat_join_request`.
+
+ .. versionadded:: 13.8
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.approve_chat_join_request(
+ user_id=self.id, chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs
+ )
+
+ def decline_join_request(
+ self,
+ chat_id: Union[int, str],
+ timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ bot.decline_chat_join_request(user_id=update.effective_user.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.decline_chat_join_request`.
+
+ .. versionadded:: 13.8
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ """
+ return self.bot.decline_chat_join_request(
+ user_id=self.id, chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs
+ )
diff --git a/telegramer/include/telegram/userprofilephotos.py b/telegramer/include/telegram/userprofilephotos.py
index ec94a0a..aa3e6ec 100644
--- a/telegramer/include/telegram/userprofilephotos.py
+++ b/telegramer/include/telegram/userprofilephotos.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,44 +18,62 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram UserProfilePhotos."""
+from typing import TYPE_CHECKING, Any, List, Optional
+
from telegram import PhotoSize, TelegramObject
+from telegram.utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
class UserProfilePhotos(TelegramObject):
"""This object represent a user's profile pictures.
- Attributes:
- total_count (:obj:`int`): Total number of profile pictures.
- photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures.
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`total_count` and :attr:`photos` are equal.
Args:
total_count (:obj:`int`): Total number of profile pictures the target user has.
photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures (in up to 4
sizes each).
+ Attributes:
+ total_count (:obj:`int`): Total number of profile pictures.
+ photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures.
+
"""
- def __init__(self, total_count, photos, **kwargs):
+ __slots__ = ('photos', 'total_count', '_id_attrs')
+
+ def __init__(self, total_count: int, photos: List[List[PhotoSize]], **_kwargs: Any):
# Required
self.total_count = int(total_count)
self.photos = photos
+ self._id_attrs = (self.total_count, self.photos)
+
@classmethod
- def de_json(cls, data, bot):
+ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['UserProfilePhotos']:
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
if not data:
return None
- data = super(UserProfilePhotos, cls).de_json(data, bot)
-
data['photos'] = [PhotoSize.de_list(photo, bot) for photo in data['photos']]
return cls(**data)
- def to_dict(self):
- data = super(UserProfilePhotos, self).to_dict()
+ def to_dict(self) -> JSONDict:
+ """See :meth:`telegram.TelegramObject.to_dict`."""
+ data = super().to_dict()
data['photos'] = []
for photo in self.photos:
data['photos'].append([x.to_dict() for x in photo])
return data
+
+ def __hash__(self) -> int:
+ return hash(tuple(tuple(p for p in photo) for photo in self.photos))
diff --git a/telegramer/include/telegram/utils/deprecate.py b/telegramer/include/telegram/utils/deprecate.py
index effd4df..bc9f519 100644
--- a/telegramer/include/telegram/utils/deprecate.py
+++ b/telegramer/include/telegram/utils/deprecate.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -25,21 +25,23 @@
# seem like it's the user that issued the warning
# We name it something else so that you don't get confused when you attempt to suppress it
class TelegramDeprecationWarning(Warning):
- pass
-
-
-def warn_deprecate_obj(old, new, stacklevel=3):
- warnings.warn(
- '{0} is being deprecated, please use {1} from now on.'.format(old, new),
- category=TelegramDeprecationWarning,
- stacklevel=stacklevel)
-
-
-def deprecate(func, old, new):
- """Warn users invoking old to switch to the new function."""
-
- def f(*args, **kwargs):
- warn_deprecate_obj(old, new)
- return func(*args, **kwargs)
-
- return f
+ """Custom warning class for deprecations in this library."""
+
+ __slots__ = ()
+
+
+# Function to warn users that setting custom attributes is deprecated (Use only in __setattr__!)
+# Checks if a custom attribute is added by checking length of dictionary before & after
+# assigning attribute. This is the fastest way to do it (I hope!).
+def set_new_attribute_deprecated(self: object, key: str, value: object) -> None:
+ """Warns the user if they set custom attributes on PTB objects."""
+ org = len(self.__dict__)
+ object.__setattr__(self, key, value)
+ new = len(self.__dict__)
+ if new > org:
+ warnings.warn(
+ f"Setting custom attributes such as {key!r} on objects such as "
+ f"{self.__class__.__name__!r} of the PTB library is deprecated.",
+ TelegramDeprecationWarning,
+ stacklevel=3,
+ )
diff --git a/telegramer/include/telegram/utils/helpers.py b/telegramer/include/telegram/utils/helpers.py
index 0290579..194a461 100644
--- a/telegramer/include/telegram/utils/helpers.py
+++ b/telegramer/include/telegram/utils/helpers.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,125 +17,580 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains helper functions."""
-from html import escape
+import datetime as dtm # dtm = "DateTime Module"
import re
import signal
-from datetime import datetime
+import time
+
+from collections import defaultdict
+from html import escape
+from pathlib import Path
+
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ DefaultDict,
+ Dict,
+ Optional,
+ Tuple,
+ Union,
+ Type,
+ cast,
+ IO,
+ TypeVar,
+ Generic,
+ overload,
+)
+
+from telegram.utils.types import JSONDict, FileInput
+
+if TYPE_CHECKING:
+ from telegram import Message, Update, TelegramObject, InputFile
+
+# in PTB-Raw we don't have pytz, so we make a little workaround here
+DTM_UTC = dtm.timezone.utc
+try:
+ import pytz
+
+ UTC = pytz.utc
+except ImportError:
+ UTC = DTM_UTC # type: ignore[assignment]
+
+try:
+ import ujson as json
+except ImportError:
+ import json # type: ignore[no-redef]
+
# From https://stackoverflow.com/questions/2549939/get-signal-names-from-numbers-in-python
-_signames = {v: k
- for k, v in reversed(sorted(vars(signal).items()))
- if k.startswith('SIG') and not k.startswith('SIG_')}
+_signames = {
+ v: k
+ for k, v in reversed(sorted(vars(signal).items()))
+ if k.startswith('SIG') and not k.startswith('SIG_')
+}
-def get_signal_name(signum):
+def get_signal_name(signum: int) -> str:
"""Returns the signal name of the given signal number."""
return _signames[signum]
-# Not using future.backports.datetime here as datetime value might be an input from the user,
-# making every isinstace() call more delicate. So we just use our own compat layer.
-if hasattr(datetime, 'timestamp'):
- # Python 3.3+
- def _timestamp(dt_obj):
- return dt_obj.timestamp()
-else:
- # Python < 3.3 (incl 2.7)
- from time import mktime
+def is_local_file(obj: Optional[Union[str, Path]]) -> bool:
+ """
+ Checks if a given string is a file on local system.
- def _timestamp(dt_obj):
- return mktime(dt_obj.timetuple())
+ Args:
+ obj (:obj:`str`): The string to check.
+ """
+ if obj is None:
+ return False
+
+ path = Path(obj)
+ try:
+ return path.is_file()
+ except Exception:
+ return False
+
+
+def parse_file_input(
+ file_input: Union[FileInput, 'TelegramObject'],
+ tg_type: Type['TelegramObject'] = None,
+ attach: bool = None,
+ filename: str = None,
+) -> Union[str, 'InputFile', Any]:
+ """
+ Parses input for sending files:
+
+ * For string input, if the input is an absolute path of a local file,
+ adds the ``file://`` prefix. If the input is a relative path of a local file, computes the
+ absolute path and adds the ``file://`` prefix. Returns the input unchanged, otherwise.
+ * :class:`pathlib.Path` objects are treated the same way as strings.
+ * For IO and bytes input, returns an :class:`telegram.InputFile`.
+ * If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id``
+ attribute.
+
+ Args:
+ file_input (:obj:`str` | :obj:`bytes` | `filelike object` | Telegram media object): The
+ input to parse.
+ tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g.
+ :class:`telegram.Animation`.
+ attach (:obj:`bool`, optional): Whether this file should be send as one file or is part of
+ a collection of files. Only relevant in case an :class:`telegram.InputFile` is
+ returned.
+ filename (:obj:`str`, optional): The filename. Only relevant in case an
+ :class:`telegram.InputFile` is returned.
+
+ Returns:
+ :obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched
+ :attr:`file_input`, in case it's no valid file input.
+ """
+ # Importing on file-level yields cyclic Import Errors
+ from telegram import InputFile # pylint: disable=C0415
+
+ if isinstance(file_input, str) and file_input.startswith('file://'):
+ return file_input
+ if isinstance(file_input, (str, Path)):
+ if is_local_file(file_input):
+ out = Path(file_input).absolute().as_uri()
+ else:
+ out = file_input # type: ignore[assignment]
+ return out
+ if isinstance(file_input, bytes):
+ return InputFile(file_input, attach=attach, filename=filename)
+ if InputFile.is_file(file_input):
+ file_input = cast(IO, file_input)
+ return InputFile(file_input, attach=attach, filename=filename)
+ if tg_type and isinstance(file_input, tg_type):
+ return file_input.file_id # type: ignore[attr-defined]
+ return file_input
+
+
+def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str:
+ """
+ Helper function to escape telegram markup symbols.
+
+ Args:
+ text (:obj:`str`): The text.
+ version (:obj:`int` | :obj:`str`): Use to specify the version of telegrams Markdown.
+ Either ``1`` or ``2``. Defaults to ``1``.
+ entity_type (:obj:`str`, optional): For the entity types ``PRE``, ``CODE`` and the link
+ part of ``TEXT_LINKS``, only certain characters need to be escaped in ``MarkdownV2``.
+ See the official API documentation for details. Only valid in combination with
+ ``version=2``, will be ignored else.
+ """
+ if int(version) == 1:
+ escape_chars = r'_*`['
+ elif int(version) == 2:
+ if entity_type in ['pre', 'code']:
+ escape_chars = r'\`'
+ elif entity_type == 'text_link':
+ escape_chars = r'\)'
+ else:
+ escape_chars = r'_*[]()~`>#+-=|{}.!'
+ else:
+ raise ValueError('Markdown version must be either 1 or 2!')
+
+ return re.sub(f'([{re.escape(escape_chars)}])', r'\\\1', text)
+
+
+# -------- date/time related helpers --------
+def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float:
+ """
+ Converts a datetime object to a float timestamp (with sub-second precision).
+ If the datetime object is timezone-naive, it is assumed to be in UTC.
+ """
+ if dt_obj.tzinfo is None:
+ dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc)
+ return dt_obj.timestamp()
-def escape_markdown(text):
- """Helper function to escape telegram markup symbols."""
- escape_chars = '\*_`\['
- return re.sub(r'([%s])' % escape_chars, r'\\\1', text)
+def _localize(datetime: dtm.datetime, tzinfo: dtm.tzinfo) -> dtm.datetime:
+ """Localize the datetime, where UTC is handled depending on whether pytz is available or not"""
+ if tzinfo is DTM_UTC:
+ return datetime.replace(tzinfo=DTM_UTC)
+ return tzinfo.localize(datetime) # type: ignore[attr-defined]
-def to_timestamp(dt_obj):
+def to_float_timestamp(
+ time_object: Union[int, float, dtm.timedelta, dtm.datetime, dtm.time],
+ reference_timestamp: float = None,
+ tzinfo: dtm.tzinfo = None,
+) -> float:
"""
+ Converts a given time object to a float POSIX timestamp.
+ Used to convert different time specifications to a common format. The time object
+ can be relative (i.e. indicate a time increment, or a time of day) or absolute.
+ object objects from the :class:`datetime` module that are timezone-naive will be assumed
+ to be in UTC, if ``bot`` is not passed or ``bot.defaults`` is :obj:`None`.
+
Args:
- dt_obj (:class:`datetime.datetime`):
+ time_object (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
+ :obj:`datetime.datetime` | :obj:`datetime.time`):
+ Time value to convert. The semantics of this parameter will depend on its type:
+
+ * :obj:`int` or :obj:`float` will be interpreted as "seconds from ``reference_t``"
+ * :obj:`datetime.timedelta` will be interpreted as
+ "time increment from ``reference_t``"
+ * :obj:`datetime.datetime` will be interpreted as an absolute date/time value
+ * :obj:`datetime.time` will be interpreted as a specific time of day
+
+ reference_timestamp (:obj:`float`, optional): POSIX timestamp that indicates the absolute
+ time from which relative calculations are to be performed (e.g. when ``t`` is given as
+ an :obj:`int`, indicating "seconds from ``reference_t``"). Defaults to now (the time at
+ which this function is called).
+
+ If ``t`` is given as an absolute representation of date & time (i.e. a
+ :obj:`datetime.datetime` object), ``reference_timestamp`` is not relevant and so its
+ value should be :obj:`None`. If this is not the case, a ``ValueError`` will be raised.
+ tzinfo (:obj:`pytz.BaseTzInfo`, optional): If ``t`` is a naive object from the
+ :class:`datetime` module, it will be interpreted as this timezone. Defaults to
+ ``pytz.utc``.
+
+ Note:
+ Only to be used by ``telegram.ext``.
+
Returns:
- int:
+ :obj:`float` | :obj:`None`:
+ The return value depends on the type of argument ``t``.
+ If ``t`` is given as a time increment (i.e. as a :obj:`int`, :obj:`float` or
+ :obj:`datetime.timedelta`), then the return value will be ``reference_t`` + ``t``.
+
+ Else if it is given as an absolute date/time value (i.e. a :obj:`datetime.datetime`
+ object), the equivalent value as a POSIX timestamp will be returned.
+ Finally, if it is a time of the day without date (i.e. a :obj:`datetime.time`
+ object), the return value is the nearest future occurrence of that time of day.
+
+ Raises:
+ TypeError: If ``t``'s type is not one of those described above.
+ ValueError: If ``t`` is a :obj:`datetime.datetime` and :obj:`reference_timestamp` is not
+ :obj:`None`.
"""
- if not dt_obj:
- return None
+ if reference_timestamp is None:
+ reference_timestamp = time.time()
+ elif isinstance(time_object, dtm.datetime):
+ raise ValueError('t is an (absolute) datetime while reference_timestamp is not None')
+
+ if isinstance(time_object, dtm.timedelta):
+ return reference_timestamp + time_object.total_seconds()
+ if isinstance(time_object, (int, float)):
+ return reference_timestamp + time_object
+
+ if tzinfo is None:
+ tzinfo = UTC
+
+ if isinstance(time_object, dtm.time):
+ reference_dt = dtm.datetime.fromtimestamp(
+ reference_timestamp, tz=time_object.tzinfo or tzinfo
+ )
+ reference_date = reference_dt.date()
+ reference_time = reference_dt.timetz()
+
+ aware_datetime = dtm.datetime.combine(reference_date, time_object)
+ if aware_datetime.tzinfo is None:
+ aware_datetime = _localize(aware_datetime, tzinfo)
+
+ # if the time of day has passed today, use tomorrow
+ if reference_time > aware_datetime.timetz():
+ aware_datetime += dtm.timedelta(days=1)
+ return _datetime_to_float_timestamp(aware_datetime)
+ if isinstance(time_object, dtm.datetime):
+ if time_object.tzinfo is None:
+ time_object = _localize(time_object, tzinfo)
+ return _datetime_to_float_timestamp(time_object)
+
+ raise TypeError(f'Unable to convert {type(time_object).__name__} object to timestamp')
+
+
+def to_timestamp(
+ dt_obj: Union[int, float, dtm.timedelta, dtm.datetime, dtm.time, None],
+ reference_timestamp: float = None,
+ tzinfo: dtm.tzinfo = None,
+) -> Optional[int]:
+ """
+ Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated
+ down to the nearest integer).
- return int(_timestamp(dt_obj))
+ See the documentation for :func:`to_float_timestamp` for more details.
+ """
+ return (
+ int(to_float_timestamp(dt_obj, reference_timestamp, tzinfo))
+ if dt_obj is not None
+ else None
+ )
-def from_timestamp(unixtime):
+def from_timestamp(unixtime: Optional[int], tzinfo: dtm.tzinfo = UTC) -> Optional[dtm.datetime]:
"""
+ Converts an (integer) unix timestamp to a timezone aware datetime object.
+ :obj:`None` s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`).
+
Args:
- unixtime (int):
+ unixtime (:obj:`int`): Integer POSIX timestamp.
+ tzinfo (:obj:`datetime.tzinfo`, optional): The timezone to which the timestamp is to be
+ converted to. Defaults to UTC.
Returns:
- datetime.datetime:
-
+ Timezone aware equivalent :obj:`datetime.datetime` value if ``unixtime`` is not
+ :obj:`None`; else :obj:`None`.
"""
- if not unixtime:
+ if unixtime is None:
return None
- return datetime.fromtimestamp(unixtime)
+ if tzinfo is not None:
+ return dtm.datetime.fromtimestamp(unixtime, tz=tzinfo)
+ return dtm.datetime.utcfromtimestamp(unixtime)
-def mention_html(user_id, name):
+# -------- end --------
+
+
+def mention_html(user_id: Union[int, str], name: str) -> str:
"""
Args:
- user_id (:obj:`int`) The user's id which you want to mention.
- name (:obj:`str`) The name the mention is showing.
+ user_id (:obj:`int`): The user's id which you want to mention.
+ name (:obj:`str`): The name the mention is showing.
Returns:
- :obj:`str`: The inline mention for the user as html.
+ :obj:`str`: The inline mention for the user as HTML.
"""
- if isinstance(user_id, int):
- return u'{}'.format(user_id, escape(name))
+ return f'{escape(name)}'
-def mention_markdown(user_id, name):
+def mention_markdown(user_id: Union[int, str], name: str, version: int = 1) -> str:
"""
Args:
- user_id (:obj:`int`) The user's id which you want to mention.
- name (:obj:`str`) The name the mention is showing.
+ user_id (:obj:`int`): The user's id which you want to mention.
+ name (:obj:`str`): The name the mention is showing.
+ version (:obj:`int` | :obj:`str`): Use to specify the version of Telegram's Markdown.
+ Either ``1`` or ``2``. Defaults to ``1``.
Returns:
- :obj:`str`: The inline mention for the user as markdown.
+ :obj:`str`: The inline mention for the user as Markdown.
"""
- if isinstance(user_id, int):
- return u'[{}](tg://user?id={})'.format(escape_markdown(name), user_id)
+ return f'[{escape_markdown(name, version=version)}](tg://user?id={user_id})'
-def effective_message_type(entity):
+def effective_message_type(entity: Union['Message', 'Update']) -> Optional[str]:
"""
Extracts the type of message as a string identifier from a :class:`telegram.Message` or a
:class:`telegram.Update`.
Args:
- entity (:obj:`Update` | :obj:`Message`) The ``update`` or ``message`` to extract from
+ entity (:class:`telegram.Update` | :class:`telegram.Message`): The ``update`` or
+ ``message`` to extract from.
Returns:
- str: One of ``Message.MESSAGE_TYPES``
+ :obj:`str`: One of ``Message.MESSAGE_TYPES``
"""
-
# Importing on file-level yields cyclic Import Errors
- from telegram import Message
- from telegram import Update
+ from telegram import Message, Update # pylint: disable=C0415
if isinstance(entity, Message):
message = entity
elif isinstance(entity, Update):
- message = entity.effective_message
+ message = entity.effective_message # type: ignore[assignment]
else:
- raise TypeError("entity is not Message or Update (got: {})".format(type(entity)))
+ raise TypeError(f"entity is not Message or Update (got: {type(entity)})")
for i in Message.MESSAGE_TYPES:
if getattr(message, i, None):
return i
return None
+
+
+def create_deep_linked_url(bot_username: str, payload: str = None, group: bool = False) -> str:
+ """
+ Creates a deep-linked URL for this ``bot_username`` with the specified ``payload``.
+ See https://core.telegram.org/bots#deep-linking to learn more.
+
+ The ``payload`` may consist of the following characters: ``A-Z, a-z, 0-9, _, -``
+
+ Note:
+ Works well in conjunction with
+ ``CommandHandler("start", callback, filters = Filters.regex('payload'))``
+
+ Examples:
+ ``create_deep_linked_url(bot.get_me().username, "some-params")``
+
+ Args:
+ bot_username (:obj:`str`): The username to link to
+ payload (:obj:`str`, optional): Parameters to encode in the created URL
+ group (:obj:`bool`, optional): If :obj:`True` the user is prompted to select a group to
+ add the bot to. If :obj:`False`, opens a one-on-one conversation with the bot.
+ Defaults to :obj:`False`.
+
+ Returns:
+ :obj:`str`: An URL to start the bot with specific parameters
+ """
+ if bot_username is None or len(bot_username) <= 3:
+ raise ValueError("You must provide a valid bot_username.")
+
+ base_url = f'https://t.me/{bot_username}'
+ if not payload:
+ return base_url
+
+ if len(payload) > 64:
+ raise ValueError("The deep-linking payload must not exceed 64 characters.")
+
+ if not re.match(r'^[A-Za-z0-9_-]+$', payload):
+ raise ValueError(
+ "Only the following characters are allowed for deep-linked "
+ "URLs: A-Z, a-z, 0-9, _ and -"
+ )
+
+ if group:
+ key = 'startgroup'
+ else:
+ key = 'start'
+
+ return f'{base_url}?{key}={payload}'
+
+
+def encode_conversations_to_json(conversations: Dict[str, Dict[Tuple, object]]) -> str:
+ """Helper method to encode a conversations dict (that uses tuples as keys) to a
+ JSON-serializable way. Use :meth:`decode_conversations_from_json` to decode.
+
+ Args:
+ conversations (:obj:`dict`): The conversations dict to transform to JSON.
+
+ Returns:
+ :obj:`str`: The JSON-serialized conversations dict
+ """
+ tmp: Dict[str, JSONDict] = {}
+ for handler, states in conversations.items():
+ tmp[handler] = {}
+ for key, state in states.items():
+ tmp[handler][json.dumps(key)] = state
+ return json.dumps(tmp)
+
+
+def decode_conversations_from_json(json_string: str) -> Dict[str, Dict[Tuple, object]]:
+ """Helper method to decode a conversations dict (that uses tuples as keys) from a
+ JSON-string created with :meth:`encode_conversations_to_json`.
+
+ Args:
+ json_string (:obj:`str`): The conversations dict as JSON string.
+
+ Returns:
+ :obj:`dict`: The conversations dict after decoding
+ """
+ tmp = json.loads(json_string)
+ conversations: Dict[str, Dict[Tuple, object]] = {}
+ for handler, states in tmp.items():
+ conversations[handler] = {}
+ for key, state in states.items():
+ conversations[handler][tuple(json.loads(key))] = state
+ return conversations
+
+
+def decode_user_chat_data_from_json(data: str) -> DefaultDict[int, Dict[object, object]]:
+ """Helper method to decode chat or user data (that uses ints as keys) from a
+ JSON-string.
+
+ Args:
+ data (:obj:`str`): The user/chat_data dict as JSON string.
+
+ Returns:
+ :obj:`dict`: The user/chat_data defaultdict after decoding
+ """
+ tmp: DefaultDict[int, Dict[object, object]] = defaultdict(dict)
+ decoded_data = json.loads(data)
+ for user, user_data in decoded_data.items():
+ user = int(user)
+ tmp[user] = {}
+ for key, value in user_data.items():
+ try:
+ key = int(key)
+ except ValueError:
+ pass
+ tmp[user][key] = value
+ return tmp
+
+
+DVType = TypeVar('DVType', bound=object)
+OT = TypeVar('OT', bound=object)
+
+
+class DefaultValue(Generic[DVType]):
+ """Wrapper for immutable default arguments that allows to check, if the default value was set
+ explicitly. Usage::
+
+ DefaultOne = DefaultValue(1)
+ def f(arg=DefaultOne):
+ if arg is DefaultOne:
+ print('`arg` is the default')
+ arg = arg.value
+ else:
+ print('`arg` was set explicitly')
+ print(f'`arg` = {str(arg)}')
+
+ This yields::
+
+ >>> f()
+ `arg` is the default
+ `arg` = 1
+ >>> f(1)
+ `arg` was set explicitly
+ `arg` = 1
+ >>> f(2)
+ `arg` was set explicitly
+ `arg` = 2
+
+ Also allows to evaluate truthiness::
+
+ default = DefaultValue(value)
+ if default:
+ ...
+
+ is equivalent to::
+
+ default = DefaultValue(value)
+ if value:
+ ...
+
+ ``repr(DefaultValue(value))`` returns ``repr(value)`` and ``str(DefaultValue(value))`` returns
+ ``f'DefaultValue({value})'``.
+
+ Args:
+ value (:obj:`obj`): The value of the default argument
+
+ Attributes:
+ value (:obj:`obj`): The value of the default argument
+
+ """
+
+ __slots__ = ('value', '__dict__')
+
+ def __init__(self, value: DVType = None):
+ self.value = value
+
+ def __bool__(self) -> bool:
+ return bool(self.value)
+
+ @overload
+ @staticmethod
+ def get_value(obj: 'DefaultValue[OT]') -> OT:
+ ...
+
+ @overload
+ @staticmethod
+ def get_value(obj: OT) -> OT:
+ ...
+
+ @staticmethod
+ def get_value(obj: Union[OT, 'DefaultValue[OT]']) -> OT:
+ """
+ Shortcut for::
+
+ return obj.value if isinstance(obj, DefaultValue) else obj
+
+ Args:
+ obj (:obj:`object`): The object to process
+
+ Returns:
+ Same type as input, or the value of the input: The value
+ """
+ return obj.value if isinstance(obj, DefaultValue) else obj # type: ignore[return-value]
+
+ # This is mostly here for readability during debugging
+ def __str__(self) -> str:
+ return f'DefaultValue({self.value})'
+
+ # This is here to have the default instances nicely rendered in the docs
+ def __repr__(self) -> str:
+ return repr(self.value)
+
+
+DEFAULT_NONE: DefaultValue = DefaultValue(None)
+""":class:`DefaultValue`: Default :obj:`None`"""
+
+DEFAULT_FALSE: DefaultValue = DefaultValue(False)
+""":class:`DefaultValue`: Default :obj:`False`"""
+
+DEFAULT_20: DefaultValue = DefaultValue(20)
+""":class:`DefaultValue`: Default :obj:`20`"""
diff --git a/telegramer/include/telegram/utils/promise.py b/telegramer/include/telegram/utils/promise.py
index 4e3ebf9..01bef33 100644
--- a/telegramer/include/telegram/utils/promise.py
+++ b/telegramer/include/telegram/utils/promise.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,77 +16,23 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
-"""This module contains the Promise class."""
-
-import logging
-from threading import Event
-
-
-logger = logging.getLogger(__name__)
-logger.addHandler(logging.NullHandler())
-
-
-class Promise(object):
- """A simple Promise implementation for use with the run_async decorator, DelayQueue etc.
-
- Args:
- pooled_function (:obj:`callable`): The callable that will be called concurrently.
- args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`.
- kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`.
-
- Attributes:
- pooled_function (:obj:`callable`): The callable that will be called concurrently.
- args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`.
- kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`.
- done (:obj:`threading.Event`): Is set when the result is available.
-
- """
-
- def __init__(self, pooled_function, args, kwargs):
- self.pooled_function = pooled_function
- self.args = args
- self.kwargs = kwargs
- self.done = Event()
- self._result = None
- self._exception = None
-
- def run(self):
- """Calls the :attr:`pooled_function` callable."""
-
- try:
- self._result = self.pooled_function(*self.args, **self.kwargs)
-
- except Exception as exc:
- logger.exception('An uncaught error was raised while running the promise')
- self._exception = exc
-
- finally:
- self.done.set()
-
- def __call__(self):
- self.run()
-
- def result(self, timeout=None):
- """Return the result of the ``Promise``.
-
- Args:
- timeout (:obj:`float`, optional): Maximum time in seconds to wait for the result to be
- calculated. ``None`` means indefinite. Default is ``None``.
-
- Returns:
- Returns the return value of :attr:`pooled_function` or ``None`` if the ``timeout``
- expires.
-
- Raises:
- Any exception raised by :attr:`pooled_function`.
- """
- self.done.wait(timeout=timeout)
- if self._exception is not None:
- raise self._exception # pylint: disable=raising-bad-type
- return self._result
-
- @property
- def exception(self):
- """The exception raised by :attr:`pooled_function` or ``None`` if no exception has been
- raised (yet)."""
- return self._exception
+"""This module contains the :class:`telegram.ext.utils.promise.Promise` class for backwards
+compatibility.
+"""
+import warnings
+
+import telegram.ext.utils.promise as promise
+from telegram.utils.deprecate import TelegramDeprecationWarning
+
+warnings.warn(
+ 'telegram.utils.promise is deprecated. Please use telegram.ext.utils.promise instead.',
+ TelegramDeprecationWarning,
+)
+
+Promise = promise.Promise
+"""
+:class:`telegram.ext.utils.promise.Promise`
+
+.. deprecated:: v13.2
+ Use :class:`telegram.ext.utils.promise.Promise` instead.
+"""
diff --git a/telegramer/include/telegram/utils/request.py b/telegramer/include/telegram/utils/request.py
index e97cfbe..4b8de7b 100644
--- a/telegramer/include/telegram/utils/request.py
+++ b/telegramer/include/telegram/utils/request.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -22,117 +22,136 @@
import socket
import sys
import warnings
-from builtins import str # For PY2
-
-import time
-import traceback
-from deluge.log import LOG as log
try:
import ujson as json
except ImportError:
- import json
+ import json # type: ignore[no-redef]
-try:
- import certifi
-except Exception as e:
- log.error(str(e) + '\n' + traceback.format_exc())
+from typing import Any, Union
+
+import certifi
try:
import telegram.vendor.ptb_urllib3.urllib3 as urllib3
import telegram.vendor.ptb_urllib3.urllib3.contrib.appengine as appengine
from telegram.vendor.ptb_urllib3.urllib3.connection import HTTPConnection
+ from telegram.vendor.ptb_urllib3.urllib3.fields import RequestField
from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout
except ImportError: # pragma: no cover
- warnings.warn("python-telegram-bot wasn't properly installed. Please refer to README.rst on "
- "how to properly install.")
- raise
+ try:
+ import urllib3 # type: ignore[no-redef]
+ import urllib3.contrib.appengine as appengine # type: ignore[no-redef]
+ from urllib3.connection import HTTPConnection # type: ignore[no-redef]
+ from urllib3.fields import RequestField # type: ignore[no-redef]
+ from urllib3.util.timeout import Timeout # type: ignore[no-redef]
+
+ warnings.warn(
+ 'python-telegram-bot is using upstream urllib3. This is allowed but not '
+ 'supported by python-telegram-bot maintainers.'
+ )
+ except ImportError:
+ warnings.warn(
+ "python-telegram-bot wasn't properly installed. Please refer to README.rst on "
+ "how to properly install."
+ )
+ raise
+
+# pylint: disable=C0412
+from telegram import InputFile, TelegramError
+from telegram.error import (
+ BadRequest,
+ ChatMigrated,
+ Conflict,
+ InvalidToken,
+ NetworkError,
+ RetryAfter,
+ TimedOut,
+ Unauthorized,
+)
+from telegram.utils.types import JSONDict
+from telegram.utils.deprecate import set_new_attribute_deprecated
+
+
+def _render_part(self: RequestField, name: str, value: str) -> str: # pylint: disable=W0613
+ r"""
+ Monkey patch urllib3.urllib3.fields.RequestField to make it *not* support RFC2231 compliant
+ Content-Disposition headers since telegram servers don't understand it. Instead just escape
+ \\ and " and replace any \n and \r with a space.
+ """
+ value = value.replace('\\', '\\\\').replace('"', '\\"')
+ value = value.replace('\r', ' ').replace('\n', ' ')
+ return f'{name}="{value}"'
-from telegram import (InputFile, TelegramError, InputMedia)
-from telegram.error import (Unauthorized, NetworkError, TimedOut, BadRequest, ChatMigrated,
- RetryAfter, InvalidToken)
-logging.getLogger('urllib3').setLevel(logging.WARNING)
+RequestField._render_part = _render_part # type: ignore # pylint: disable=W0212
-USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + \
- '(KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
+logging.getLogger('telegram.vendor.ptb_urllib3.urllib3').setLevel(logging.WARNING)
+USER_AGENT = 'Python Telegram Bot (https://github.com/python-telegram-bot/python-telegram-bot)'
-class Request(object):
+
+class Request:
"""
Helper class for python-telegram-bot which provides methods to perform POST & GET towards
- telegram servers.
+ Telegram servers.
Args:
- con_pool_size (int): Number of connections to keep in the connection pool.
- proxy_url (str): The URL to the proxy server. For example: `http://127.0.0.1:3128`.
- urllib3_proxy_kwargs (dict): Arbitrary arguments passed as-is to `urllib3.ProxyManager`.
- This value will be ignored if proxy_url is not set.
- connect_timeout (int|float): The maximum amount of time (in seconds) to wait for a
- connection attempt to a server to succeed. None will set an infinite timeout for
- connection attempts. (default: 5.)
- read_timeout (int|float): The maximum amount of time (in seconds) to wait between
- consecutive read operations for a response from the server. None will set an infinite
- timeout. This value is usually overridden by the various ``telegram.Bot`` methods.
- (default: 5.)
+ con_pool_size (:obj:`int`): Number of connections to keep in the connection pool.
+ proxy_url (:obj:`str`): The URL to the proxy server. For example: `http://127.0.0.1:3128`.
+ urllib3_proxy_kwargs (:obj:`dict`): Arbitrary arguments passed as-is to
+ :obj:`urllib3.ProxyManager`. This value will be ignored if :attr:`proxy_url` is not
+ set.
+ connect_timeout (:obj:`int` | :obj:`float`): The maximum amount of time (in seconds) to
+ wait for a connection attempt to a server to succeed. :obj:`None` will set an
+ infinite timeout for connection attempts. Defaults to ``5.0``.
+ read_timeout (:obj:`int` | :obj:`float`): The maximum amount of time (in seconds) to wait
+ between consecutive read operations for a response from the server. :obj:`None` will
+ set an infinite timeout. This value is usually overridden by the various
+ :class:`telegram.Bot` methods. Defaults to ``5.0``.
"""
- def __init__(self,
- con_pool_size=1,
- proxy_url=None,
- urllib3_proxy_kwargs=None,
- connect_timeout=5.,
- read_timeout=5.):
+ __slots__ = ('_connect_timeout', '_con_pool_size', '_con_pool', '__dict__')
+
+ def __init__(
+ self,
+ con_pool_size: int = 1,
+ proxy_url: str = None,
+ urllib3_proxy_kwargs: JSONDict = None,
+ connect_timeout: float = 5.0,
+ read_timeout: float = 5.0,
+ ):
if urllib3_proxy_kwargs is None:
- urllib3_proxy_kwargs = dict()
+ urllib3_proxy_kwargs = {}
self._connect_timeout = connect_timeout
sockopts = HTTPConnection.default_socket_options + [
- (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]
+ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
+ ]
# TODO: Support other platforms like mac and windows.
if 'linux' in sys.platform:
- sockopts.append((socket.IPPROTO_TCP,
- socket.TCP_KEEPIDLE, 120)) # pylint: disable=no-member
- sockopts.append((socket.IPPROTO_TCP,
- socket.TCP_KEEPINTVL, 30)) # pylint: disable=no-member
- sockopts.append((socket.IPPROTO_TCP,
- socket.TCP_KEEPCNT, 8)) # pylint: disable=no-member
+ sockopts.append(
+ (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 120) # pylint: disable=no-member
+ )
+ sockopts.append(
+ (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) # pylint: disable=no-member
+ )
+ sockopts.append(
+ (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 8) # pylint: disable=no-member
+ )
self._con_pool_size = con_pool_size
- # This was performed on Windows only, but it shouldn't be a problem
- # managing cacert.pem on Linux as well
- # if os.name == 'nt':
- try:
- import urllib2
- import tempfile
- capath = os.path.join(tempfile.gettempdir(), 'tg-cacert.pem')
- # Check if tg-cacert.pem exists and if it's older than 7 days
- if not os.path.exists(capath) or (os.path.exists(capath)
- and (time.time() - os.path.getctime(capath)) // (24 * 3600) >= 7):
- CACERT_URL = "https://curl.haxx.se/ca/cacert.pem"
- request = urllib2.Request(CACERT_URL)
- file_contents = urllib2.urlopen(request).read()
- log.debug("## Telegramer downloaded "+os.path.realpath(capath))
- cafile = open(os.path.realpath(capath), 'wb')
- cafile.write(file_contents)
- cafile.close()
- except Exception as e:
- try:
- capath = certifi.where()
- except Exception as e:
- capath = os.path.join(tempfile.gettempdir(), 'tg-cacert.pem')
-
kwargs = dict(
maxsize=con_pool_size,
cert_reqs='CERT_REQUIRED',
- ca_certs=capath, # certifi.where(),
+ ca_certs=certifi.where(),
socket_options=sockopts,
- timeout=urllib3.Timeout(
- connect=self._connect_timeout, read=read_timeout, total=None))
+ timeout=urllib3.Timeout(connect=self._connect_timeout, read=read_timeout, total=None),
+ )
# Set a proxy according to the following order:
# * proxy defined in proxy_url (+ urllib3_proxy_kwargs)
@@ -143,20 +162,27 @@ def __init__(self,
if not proxy_url:
proxy_url = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
+ self._con_pool: Union[
+ urllib3.PoolManager,
+ appengine.AppEngineManager,
+ 'SOCKSProxyManager', # noqa: F821
+ urllib3.ProxyManager,
+ ] = None # type: ignore
if not proxy_url:
if appengine.is_appengine_sandbox():
# Use URLFetch service if running in App Engine
- mgr = appengine.AppEngineManager()
+ self._con_pool = appengine.AppEngineManager()
else:
- mgr = urllib3.PoolManager(**kwargs)
+ self._con_pool = urllib3.PoolManager(**kwargs)
else:
kwargs.update(urllib3_proxy_kwargs)
if proxy_url.startswith('socks'):
try:
+ # pylint: disable=C0415
from telegram.vendor.ptb_urllib3.urllib3.contrib.socks import SOCKSProxyManager
- except ImportError:
- raise RuntimeError('PySocks is missing')
- mgr = SOCKSProxyManager(proxy_url, **kwargs)
+ except ImportError as exc:
+ raise RuntimeError('PySocks is missing') from exc
+ self._con_pool = SOCKSProxyManager(proxy_url, **kwargs)
else:
mgr = urllib3.proxy_from_url(proxy_url, **kwargs)
if mgr.proxy.auth:
@@ -164,34 +190,33 @@ def __init__(self,
auth_hdrs = urllib3.make_headers(proxy_basic_auth=mgr.proxy.auth)
mgr.proxy_headers.update(auth_hdrs)
- self._con_pool = mgr
+ self._con_pool = mgr
+
+ def __setattr__(self, key: str, value: object) -> None:
+ set_new_attribute_deprecated(self, key, value)
@property
- def con_pool_size(self):
+ def con_pool_size(self) -> int:
"""The size of the connection pool used."""
return self._con_pool_size
- def stop(self):
- self._con_pool.clear()
+ def stop(self) -> None:
+ """Performs cleanup on shutdown."""
+ self._con_pool.clear() # type: ignore
@staticmethod
- def _parse(json_data):
+ def _parse(json_data: bytes) -> Union[JSONDict, bool]:
"""Try and parse the JSON returned from Telegram.
Returns:
dict: A JSON parsed as Python dict with results - on error this dict will be empty.
"""
-
+ decoded_s = json_data.decode('utf-8', 'replace')
try:
- decoded_s = json_data.decode('utf-8')
data = json.loads(decoded_s)
- except UnicodeDecodeError:
- logging.getLogger(__name__).debug(
- 'Logging raw invalid UTF-8 response:\n%r', json_data)
- raise TelegramError('Server response could not be decoded using UTF-8')
- except ValueError:
- raise TelegramError('Invalid server response')
+ except ValueError as exc:
+ raise TelegramError('Invalid server response') from exc
if not data.get('ok'): # pragma: no cover
description = data.get('description')
@@ -208,15 +233,15 @@ def _parse(json_data):
return data['result']
- def _request_wrapper(self, *args, **kwargs):
+ def _request_wrapper(self, *args: object, **kwargs: Any) -> bytes:
"""Wraps urllib3 request for handling known exceptions.
Args:
args: unnamed arguments, passed to urllib3 request.
- kwargs: keyword arguments, passed tp urllib3 request.
+ kwargs: keyword arguments, passed to urllib3 request.
Returns:
- str: A non-parsed JSON text.
+ bytes: A non-parsed JSON text.
Raises:
TelegramError
@@ -232,45 +257,48 @@ def _request_wrapper(self, *args, **kwargs):
try:
resp = self._con_pool.request(*args, **kwargs)
- except urllib3.exceptions.TimeoutError:
- raise TimedOut()
+ except urllib3.exceptions.TimeoutError as error:
+ raise TimedOut() from error
except urllib3.exceptions.HTTPError as error:
# HTTPError must come last as its the base urllib3 exception class
# TODO: do something smart here; for now just raise NetworkError
- raise NetworkError('urllib3 HTTPError {0}'.format(error))
+ raise NetworkError(f'urllib3 HTTPError {error}') from error
if 200 <= resp.status <= 299:
# 200-299 range are HTTP success statuses
return resp.data
try:
- message = self._parse(resp.data)
+ message = str(self._parse(resp.data))
except ValueError:
message = 'Unknown HTTPError'
if resp.status in (401, 403):
raise Unauthorized(message)
- elif resp.status == 400:
+ if resp.status == 400:
raise BadRequest(message)
- elif resp.status == 404:
+ if resp.status == 404:
raise InvalidToken()
- elif resp.status == 413:
- raise NetworkError('File too large. Check telegram api limits '
- 'https://core.telegram.org/bots/api#senddocument')
-
- elif resp.status == 502:
+ if resp.status == 409:
+ raise Conflict(message)
+ if resp.status == 413:
+ raise NetworkError(
+ 'File too large. Check telegram api limits '
+ 'https://core.telegram.org/bots/api#senddocument'
+ )
+ if resp.status == 502:
raise NetworkError('Bad Gateway')
- else:
- raise NetworkError('{0} ({1})'.format(message, resp.status))
+ raise NetworkError(f'{message} ({resp.status})')
- def get(self, url, timeout=None):
+ def post(self, url: str, data: JSONDict, timeout: float = None) -> Union[JSONDict, bool]:
"""Request an URL.
Args:
url (:obj:`str`): The web location we want to retrieve.
- timeout (:obj:`int` | :obj:`float`): If this value is specified, use it as the read
- timeout from the server (instead of the one specified during creation of the
- connection pool).
+ data (Dict[:obj:`str`, :obj:`str` | :obj:`int`], optional): A dict of key/value pairs.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
Returns:
A JSON object.
@@ -281,31 +309,13 @@ def get(self, url, timeout=None):
if timeout is not None:
urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout)
- result = self._request_wrapper('GET', url, **urlopen_kwargs)
- return self._parse(result)
-
- def post(self, url, data, timeout=None):
- """Request an URL.
-
- Args:
- url (:obj:`str`): The web location we want to retrieve.
- data (dict[str, str|int]): A dict of key/value pairs. Note: On py2.7 value is unicode.
- timeout (:obj:`int` | :obj:`float`): If this value is specified, use it as the read
- timeout from the server (instead of the one specified during creation of the
- connection pool).
-
- Returns:
- A JSON object.
-
- """
- urlopen_kwargs = {}
-
- if timeout is not None:
- urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout)
+ if data is None:
+ data = {}
# Are we uploading files?
files = False
+ # pylint: disable=R1702
for key, val in data.copy().items():
if isinstance(val, InputFile):
# Convert the InputFile to urllib3 field format
@@ -315,33 +325,50 @@ def post(self, url, data, timeout=None):
# Urllib3 doesn't like floats it seems
data[key] = str(val)
elif key == 'media':
- # One media or multiple
- if isinstance(val, InputMedia):
- # Attach and set val to attached name
- data[key] = val.to_json()
- if isinstance(val.media, InputFile):
- data[val.media.attach] = val.media.field_tuple
- else:
+ files = True
+ # List of media
+ if isinstance(val, list):
# Attach and set val to attached name for all
media = []
- for m in val:
- media.append(m.to_dict())
- if isinstance(m.media, InputFile):
- data[m.media.attach] = m.media.field_tuple
+ for med in val:
+ media_dict = med.to_dict()
+ media.append(media_dict)
+ if isinstance(med.media, InputFile):
+ data[med.media.attach] = med.media.field_tuple
+ # if the file has a thumb, we also need to attach it to the data
+ if "thumb" in media_dict:
+ data[med.thumb.attach] = med.thumb.field_tuple
data[key] = json.dumps(media)
- files = True
+ # Single media
+ else:
+ # Attach and set val to attached name
+ media_dict = val.to_dict()
+ if isinstance(val.media, InputFile):
+ data[val.media.attach] = val.media.field_tuple
+ # if the file has a thumb, we also need to attach it to the data
+ if "thumb" in media_dict:
+ data[val.thumb.attach] = val.thumb.field_tuple
+ data[key] = json.dumps(media_dict)
+ elif isinstance(val, list):
+ # In case we're sending files, we need to json-dump lists manually
+ # As we can't know if that's the case, we just json-dump here
+ data[key] = json.dumps(val)
# Use multipart upload if we're uploading files, otherwise use JSON
if files:
result = self._request_wrapper('POST', url, fields=data, **urlopen_kwargs)
else:
- result = self._request_wrapper('POST', url,
- body=json.dumps(data).encode('utf-8'),
- headers={'Content-Type': 'application/json'})
+ result = self._request_wrapper(
+ 'POST',
+ url,
+ body=json.dumps(data).encode('utf-8'),
+ headers={'Content-Type': 'application/json'},
+ **urlopen_kwargs,
+ )
return self._parse(result)
- def retrieve(self, url, timeout=None):
+ def retrieve(self, url: str, timeout: float = None) -> bytes:
"""Retrieve the contents of a file by its URL.
Args:
@@ -357,17 +384,15 @@ def retrieve(self, url, timeout=None):
return self._request_wrapper('GET', url, **urlopen_kwargs)
- def download(self, url, filename, timeout=None):
+ def download(self, url: str, filename: str, timeout: float = None) -> None:
"""Download a file by its URL.
Args:
- url (str): The web location we want to retrieve.
- timeout (:obj:`int` | :obj:`float`): If this value is specified, use it as the read
- timeout from the server (instead of the one specified during creation of the
- connection pool).
-
- filename:
- The filename within the path to download the file.
+ url (:obj:`str`): The web location we want to retrieve.
+ timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
+ the read timeout from the server (instead of the one specified during creation of
+ the connection pool).
+ filename (:obj:`str`): The filename within the path to download the file.
"""
buf = self.retrieve(url, timeout=timeout)
diff --git a/telegramer/include/telegram/utils/types.py b/telegramer/include/telegram/utils/types.py
new file mode 100644
index 0000000..a3e1f42
--- /dev/null
+++ b/telegramer/include/telegram/utils/types.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains custom typing aliases."""
+from pathlib import Path
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Dict,
+ List,
+ Optional,
+ Tuple,
+ TypeVar,
+ Union,
+)
+
+if TYPE_CHECKING:
+ from telegram import InputFile # noqa: F401
+ from telegram.utils.helpers import DefaultValue # noqa: F401
+
+FileLike = Union[IO, 'InputFile']
+"""Either an open file handler or a :class:`telegram.InputFile`."""
+
+FileInput = Union[str, bytes, FileLike, Path]
+"""Valid input for passing files to Telegram. Either a file id as string, a file like object,
+a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`."""
+
+JSONDict = Dict[str, Any]
+"""Dictionary containing response from Telegram or data to send to the API."""
+
+DVType = TypeVar('DVType')
+ODVInput = Optional[Union['DefaultValue[DVType]', DVType]]
+"""Generic type for bot method parameters which can have defaults. ``ODVInput[type]`` is the same
+as ``Optional[Union[DefaultValue, type]]``."""
+DVInput = Union['DefaultValue[DVType]', DVType]
+"""Generic type for bot method parameters which can have defaults. ``DVInput[type]`` is the same
+as ``Union[DefaultValue, type]``."""
+
+RT = TypeVar("RT")
+SLT = Union[RT, List[RT], Tuple[RT, ...]]
+"""Single instance or list/tuple of instances."""
diff --git a/telegramer/include/telegram/utils/webhookhandler.py b/telegramer/include/telegram/utils/webhookhandler.py
index 8adc897..88e3c5a 100644
--- a/telegramer/include/telegram/utils/webhookhandler.py
+++ b/telegramer/include/telegram/utils/webhookhandler.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,137 +16,20 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
-import logging
-
-from telegram import Update
-# REMREM from future.utils import bytes_to_native_str
-try:
- from future.utils import bytes_to_native_str
-except Exception as e:
- pass
-from threading import Lock
-try:
- import ujson as json
-except ImportError:
- import json
-try:
- import BaseHTTPServer
-except ImportError:
- import http.server as BaseHTTPServer
-
-logging.getLogger(__name__).addHandler(logging.NullHandler())
-
-
-class _InvalidPost(Exception):
-
- def __init__(self, http_code):
- self.http_code = http_code
- super(_InvalidPost, self).__init__()
-
-
-class WebhookServer(BaseHTTPServer.HTTPServer, object):
-
- def __init__(self, server_address, RequestHandlerClass, update_queue, webhook_path, bot):
- super(WebhookServer, self).__init__(server_address, RequestHandlerClass)
- self.logger = logging.getLogger(__name__)
- self.update_queue = update_queue
- self.webhook_path = webhook_path
- self.bot = bot
- self.is_running = False
- self.server_lock = Lock()
- self.shutdown_lock = Lock()
-
- def serve_forever(self, poll_interval=0.5):
- with self.server_lock:
- self.is_running = True
- self.logger.debug('Webhook Server started.')
- super(WebhookServer, self).serve_forever(poll_interval)
- self.logger.debug('Webhook Server stopped.')
-
- def shutdown(self):
- with self.shutdown_lock:
- if not self.is_running:
- self.logger.warning('Webhook Server already stopped.')
- return
- else:
- super(WebhookServer, self).shutdown()
- self.is_running = False
-
- def handle_error(self, request, client_address):
- """Handle an error gracefully."""
- self.logger.debug('Exception happened during processing of request from %s',
- client_address, exc_info=True)
-
-
-# WebhookHandler, process webhook calls
-# Based on: https://github.com/eternnoir/pyTelegramBotAPI/blob/master/
-# examples/webhook_examples/webhook_cpython_echo_bot.py
-class WebhookHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
- server_version = 'WebhookHandler/1.0'
-
- def __init__(self, request, client_address, server):
- self.logger = logging.getLogger(__name__)
- super(WebhookHandler, self).__init__(request, client_address, server)
-
- def do_HEAD(self):
- self.send_response(200)
- self.end_headers()
-
- def do_GET(self):
- self.send_response(200)
- self.end_headers()
-
- def do_POST(self):
- self.logger.debug('Webhook triggered')
- try:
- self._validate_post()
- clen = self._get_content_len()
- except _InvalidPost as e:
- self.send_error(e.http_code)
- self.end_headers()
- else:
- buf = self.rfile.read(clen)
- json_string = bytes_to_native_str(buf)
-
- self.send_response(200)
- self.end_headers()
-
- self.logger.debug('Webhook received data: ' + json_string)
-
- update = Update.de_json(json.loads(json_string), self.server.bot)
-
- self.logger.debug('Received Update with ID %d on Webhook' % update.update_id)
- self.server.update_queue.put(update)
-
- def _validate_post(self):
- if not (self.path == self.server.webhook_path and 'content-type' in self.headers and
- self.headers['content-type'] == 'application/json'):
- raise _InvalidPost(403)
-
- def _get_content_len(self):
- clen = self.headers.get('content-length')
- if clen is None:
- raise _InvalidPost(411)
- try:
- clen = int(clen)
- except ValueError:
- raise _InvalidPost(403)
- if clen < 0:
- raise _InvalidPost(403)
- return clen
-
- def log_message(self, format, *args):
- """Log an arbitrary message.
-
- This is used by all other logging functions.
-
- It overrides ``BaseHTTPRequestHandler.log_message``, which logs to ``sys.stderr``.
-
- The first argument, FORMAT, is a format string for the message to be logged. If the format
- string contains any % escapes requiring parameters, they should be specified as subsequent
- arguments (it's just like printf!).
-
- The client ip is prefixed to every message.
-
- """
- self.logger.debug("%s - - %s" % (self.address_string(), format % args))
+"""This module contains the :class:`telegram.ext.utils.webhookhandler.WebhookHandler` class for
+backwards compatibility.
+"""
+import warnings
+
+import telegram.ext.utils.webhookhandler as webhook_handler
+from telegram.utils.deprecate import TelegramDeprecationWarning
+
+warnings.warn(
+ 'telegram.utils.webhookhandler is deprecated. Please use telegram.ext.utils.webhookhandler '
+ 'instead.',
+ TelegramDeprecationWarning,
+)
+
+WebhookHandler = webhook_handler.WebhookHandler
+WebhookServer = webhook_handler.WebhookServer
+WebhookAppClass = webhook_handler.WebhookAppClass
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/__init__.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/__init__.py
index 7dc5a70..0cd5e34 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/__init__.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/__init__.py
@@ -1,96 +1,96 @@
-"""
-urllib3 - Thread-safe connection pooling and re-using.
-"""
-from __future__ import absolute_import
-import warnings
-
-from .connectionpool import (
- HTTPConnectionPool,
- HTTPSConnectionPool,
- connection_from_url
-)
-
-from . import exceptions
-from .filepost import encode_multipart_formdata
-from .poolmanager import PoolManager, ProxyManager, proxy_from_url
-from .response import HTTPResponse
-from .util.request import make_headers
-from .util.url import get_host
-from .util.timeout import Timeout
-from .util.retry import Retry
-
-
-# Set default logging handler to avoid "No handler found" warnings.
-import logging
-try: # Python 2.7+
- from logging import NullHandler
-except ImportError:
- class NullHandler(logging.Handler):
- def emit(self, record):
- pass
-
-__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
-__license__ = 'MIT'
-__version__ = 'dev'
-
-__all__ = (
- 'HTTPConnectionPool',
- 'HTTPSConnectionPool',
- 'PoolManager',
- 'ProxyManager',
- 'HTTPResponse',
- 'Retry',
- 'Timeout',
- 'add_stderr_logger',
- 'connection_from_url',
- 'disable_warnings',
- 'encode_multipart_formdata',
- 'get_host',
- 'make_headers',
- 'proxy_from_url',
-)
-
-logging.getLogger(__name__).addHandler(NullHandler())
-
-
-def add_stderr_logger(level=logging.DEBUG):
- """
- Helper for quickly adding a StreamHandler to the logger. Useful for
- debugging.
-
- Returns the handler after adding it.
- """
- # This method needs to be in this __init__.py to get the __name__ correct
- # even if urllib3 is vendored within another package.
- logger = logging.getLogger(__name__)
- handler = logging.StreamHandler()
- handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
- logger.addHandler(handler)
- logger.setLevel(level)
- logger.debug('Added a stderr logging handler to logger: %s', __name__)
- return handler
-
-
-# ... Clean up.
-del NullHandler
-
-
-# All warning filters *must* be appended unless you're really certain that they
-# shouldn't be: otherwise, it's very hard for users to use most Python
-# mechanisms to silence them.
-# SecurityWarning's always go off by default.
-warnings.simplefilter('always', exceptions.SecurityWarning, append=True)
-# SubjectAltNameWarning's should go off once per host
-warnings.simplefilter('default', exceptions.SubjectAltNameWarning, append=True)
-# InsecurePlatformWarning's don't vary between requests, so we keep it default.
-warnings.simplefilter('default', exceptions.InsecurePlatformWarning,
- append=True)
-# SNIMissingWarnings should go off only once.
-warnings.simplefilter('default', exceptions.SNIMissingWarning, append=True)
-
-
-def disable_warnings(category=exceptions.HTTPWarning):
- """
- Helper for quickly disabling all urllib3 warnings.
- """
- warnings.simplefilter('ignore', category)
+"""
+urllib3 - Thread-safe connection pooling and re-using.
+"""
+from __future__ import absolute_import
+import warnings
+
+from .connectionpool import (
+ HTTPConnectionPool,
+ HTTPSConnectionPool,
+ connection_from_url
+)
+
+from . import exceptions
+from .filepost import encode_multipart_formdata
+from .poolmanager import PoolManager, ProxyManager, proxy_from_url
+from .response import HTTPResponse
+from .util.request import make_headers
+from .util.url import get_host
+from .util.timeout import Timeout
+from .util.retry import Retry
+
+
+# Set default logging handler to avoid "No handler found" warnings.
+import logging
+try: # Python 2.7+
+ from logging import NullHandler
+except ImportError:
+ class NullHandler(logging.Handler):
+ def emit(self, record):
+ pass
+
+__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
+__license__ = 'MIT'
+__version__ = 'dev'
+
+__all__ = (
+ 'HTTPConnectionPool',
+ 'HTTPSConnectionPool',
+ 'PoolManager',
+ 'ProxyManager',
+ 'HTTPResponse',
+ 'Retry',
+ 'Timeout',
+ 'add_stderr_logger',
+ 'connection_from_url',
+ 'disable_warnings',
+ 'encode_multipart_formdata',
+ 'get_host',
+ 'make_headers',
+ 'proxy_from_url',
+)
+
+logging.getLogger(__name__).addHandler(NullHandler())
+
+
+def add_stderr_logger(level=logging.DEBUG):
+ """
+ Helper for quickly adding a StreamHandler to the logger. Useful for
+ debugging.
+
+ Returns the handler after adding it.
+ """
+ # This method needs to be in this __init__.py to get the __name__ correct
+ # even if urllib3 is vendored within another package.
+ logger = logging.getLogger(__name__)
+ handler = logging.StreamHandler()
+ handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
+ logger.addHandler(handler)
+ logger.setLevel(level)
+ logger.debug('Added a stderr logging handler to logger: %s', __name__)
+ return handler
+
+
+# ... Clean up.
+del NullHandler
+
+
+# All warning filters *must* be appended unless you're really certain that they
+# shouldn't be: otherwise, it's very hard for users to use most Python
+# mechanisms to silence them.
+# SecurityWarning's always go off by default.
+warnings.simplefilter('always', exceptions.SecurityWarning, append=True)
+# SubjectAltNameWarning's should go off once per host
+warnings.simplefilter('default', exceptions.SubjectAltNameWarning, append=True)
+# InsecurePlatformWarning's don't vary between requests, so we keep it default.
+warnings.simplefilter('default', exceptions.InsecurePlatformWarning,
+ append=True)
+# SNIMissingWarnings should go off only once.
+warnings.simplefilter('default', exceptions.SNIMissingWarning, append=True)
+
+
+def disable_warnings(category=exceptions.HTTPWarning):
+ """
+ Helper for quickly disabling all urllib3 warnings.
+ """
+ warnings.simplefilter('ignore', category)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/_collections.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/_collections.py
index 23db3a3..4fcd2ab 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/_collections.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/_collections.py
@@ -1,324 +1,327 @@
-from __future__ import absolute_import
-from collections import Mapping, MutableMapping
-try:
- from threading import RLock
-except ImportError: # Platform-specific: No threads available
- class RLock:
- def __enter__(self):
- pass
-
- def __exit__(self, exc_type, exc_value, traceback):
- pass
-
-
-try: # Python 2.7+
- from collections import OrderedDict
-except ImportError:
- from .packages.ordered_dict import OrderedDict
-from .packages.six import iterkeys, itervalues, PY3
-
-
-__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
-
-
-_Null = object()
-
-
-class RecentlyUsedContainer(MutableMapping):
- """
- Provides a thread-safe dict-like container which maintains up to
- ``maxsize`` keys while throwing away the least-recently-used keys beyond
- ``maxsize``.
-
- :param maxsize:
- Maximum number of recent elements to retain.
-
- :param dispose_func:
- Every time an item is evicted from the container,
- ``dispose_func(value)`` is called. Callback which will get called
- """
-
- ContainerCls = OrderedDict
-
- def __init__(self, maxsize=10, dispose_func=None):
- self._maxsize = maxsize
- self.dispose_func = dispose_func
-
- self._container = self.ContainerCls()
- self.lock = RLock()
-
- def __getitem__(self, key):
- # Re-insert the item, moving it to the end of the eviction line.
- with self.lock:
- item = self._container.pop(key)
- self._container[key] = item
- return item
-
- def __setitem__(self, key, value):
- evicted_value = _Null
- with self.lock:
- # Possibly evict the existing value of 'key'
- evicted_value = self._container.get(key, _Null)
- self._container[key] = value
-
- # If we didn't evict an existing value, we might have to evict the
- # least recently used item from the beginning of the container.
- if len(self._container) > self._maxsize:
- _key, evicted_value = self._container.popitem(last=False)
-
- if self.dispose_func and evicted_value is not _Null:
- self.dispose_func(evicted_value)
-
- def __delitem__(self, key):
- with self.lock:
- value = self._container.pop(key)
-
- if self.dispose_func:
- self.dispose_func(value)
-
- def __len__(self):
- with self.lock:
- return len(self._container)
-
- def __iter__(self):
- raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.')
-
- def clear(self):
- with self.lock:
- # Copy pointers to all values, then wipe the mapping
- values = list(itervalues(self._container))
- self._container.clear()
-
- if self.dispose_func:
- for value in values:
- self.dispose_func(value)
-
- def keys(self):
- with self.lock:
- return list(iterkeys(self._container))
-
-
-class HTTPHeaderDict(MutableMapping):
- """
- :param headers:
- An iterable of field-value pairs. Must not contain multiple field names
- when compared case-insensitively.
-
- :param kwargs:
- Additional field-value pairs to pass in to ``dict.update``.
-
- A ``dict`` like container for storing HTTP Headers.
-
- Field names are stored and compared case-insensitively in compliance with
- RFC 7230. Iteration provides the first case-sensitive key seen for each
- case-insensitive pair.
-
- Using ``__setitem__`` syntax overwrites fields that compare equal
- case-insensitively in order to maintain ``dict``'s api. For fields that
- compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
- in a loop.
-
- If multiple fields that are equal case-insensitively are passed to the
- constructor or ``.update``, the behavior is undefined and some will be
- lost.
-
- >>> headers = HTTPHeaderDict()
- >>> headers.add('Set-Cookie', 'foo=bar')
- >>> headers.add('set-cookie', 'baz=quxx')
- >>> headers['content-length'] = '7'
- >>> headers['SET-cookie']
- 'foo=bar, baz=quxx'
- >>> headers['Content-Length']
- '7'
- """
-
- def __init__(self, headers=None, **kwargs):
- super(HTTPHeaderDict, self).__init__()
- self._container = OrderedDict()
- if headers is not None:
- if isinstance(headers, HTTPHeaderDict):
- self._copy_from(headers)
- else:
- self.extend(headers)
- if kwargs:
- self.extend(kwargs)
-
- def __setitem__(self, key, val):
- self._container[key.lower()] = (key, val)
- return self._container[key.lower()]
-
- def __getitem__(self, key):
- val = self._container[key.lower()]
- return ', '.join(val[1:])
-
- def __delitem__(self, key):
- del self._container[key.lower()]
-
- def __contains__(self, key):
- return key.lower() in self._container
-
- def __eq__(self, other):
- if not isinstance(other, Mapping) and not hasattr(other, 'keys'):
- return False
- if not isinstance(other, type(self)):
- other = type(self)(other)
- return (dict((k.lower(), v) for k, v in self.itermerged()) ==
- dict((k.lower(), v) for k, v in other.itermerged()))
-
- def __ne__(self, other):
- return not self.__eq__(other)
-
- if not PY3: # Python 2
- iterkeys = MutableMapping.iterkeys
- itervalues = MutableMapping.itervalues
-
- __marker = object()
-
- def __len__(self):
- return len(self._container)
-
- def __iter__(self):
- # Only provide the originally cased names
- for vals in self._container.values():
- yield vals[0]
-
- def pop(self, key, default=__marker):
- '''D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
- If key is not found, d is returned if given, otherwise KeyError is raised.
- '''
- # Using the MutableMapping function directly fails due to the private marker.
- # Using ordinary dict.pop would expose the internal structures.
- # So let's reinvent the wheel.
- try:
- value = self[key]
- except KeyError:
- if default is self.__marker:
- raise
- return default
- else:
- del self[key]
- return value
-
- def discard(self, key):
- try:
- del self[key]
- except KeyError:
- pass
-
- def add(self, key, val):
- """Adds a (name, value) pair, doesn't overwrite the value if it already
- exists.
-
- >>> headers = HTTPHeaderDict(foo='bar')
- >>> headers.add('Foo', 'baz')
- >>> headers['foo']
- 'bar, baz'
- """
- key_lower = key.lower()
- new_vals = key, val
- # Keep the common case aka no item present as fast as possible
- vals = self._container.setdefault(key_lower, new_vals)
- if new_vals is not vals:
- # new_vals was not inserted, as there was a previous one
- if isinstance(vals, list):
- # If already several items got inserted, we have a list
- vals.append(val)
- else:
- # vals should be a tuple then, i.e. only one item so far
- # Need to convert the tuple to list for further extension
- self._container[key_lower] = [vals[0], vals[1], val]
-
- def extend(self, *args, **kwargs):
- """Generic import function for any type of header-like object.
- Adapted version of MutableMapping.update in order to insert items
- with self.add instead of self.__setitem__
- """
- if len(args) > 1:
- raise TypeError("extend() takes at most 1 positional "
- "arguments ({0} given)".format(len(args)))
- other = args[0] if len(args) >= 1 else ()
-
- if isinstance(other, HTTPHeaderDict):
- for key, val in other.iteritems():
- self.add(key, val)
- elif isinstance(other, Mapping):
- for key in other:
- self.add(key, other[key])
- elif hasattr(other, "keys"):
- for key in other.keys():
- self.add(key, other[key])
- else:
- for key, value in other:
- self.add(key, value)
-
- for key, value in kwargs.items():
- self.add(key, value)
-
- def getlist(self, key):
- """Returns a list of all the values for the named field. Returns an
- empty list if the key doesn't exist."""
- try:
- vals = self._container[key.lower()]
- except KeyError:
- return []
- else:
- if isinstance(vals, tuple):
- return [vals[1]]
- else:
- return vals[1:]
-
- # Backwards compatibility for httplib
- getheaders = getlist
- getallmatchingheaders = getlist
- iget = getlist
-
- def __repr__(self):
- return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
-
- def _copy_from(self, other):
- for key in other:
- val = other.getlist(key)
- if isinstance(val, list):
- # Don't need to convert tuples
- val = list(val)
- self._container[key.lower()] = [key] + val
-
- def copy(self):
- clone = type(self)()
- clone._copy_from(self)
- return clone
-
- def iteritems(self):
- """Iterate over all header lines, including duplicate ones."""
- for key in self:
- vals = self._container[key.lower()]
- for val in vals[1:]:
- yield vals[0], val
-
- def itermerged(self):
- """Iterate over all headers, merging duplicate ones together."""
- for key in self:
- val = self._container[key.lower()]
- yield val[0], ', '.join(val[1:])
-
- def items(self):
- return list(self.iteritems())
-
- @classmethod
- def from_httplib(cls, message): # Python 2
- """Read headers from a Python 2 httplib message object."""
- # python2.7 does not expose a proper API for exporting multiheaders
- # efficiently. This function re-reads raw lines from the message
- # object and extracts the multiheaders properly.
- headers = []
-
- for line in message.headers:
- if line.startswith((' ', '\t')):
- key, value = headers[-1]
- headers[-1] = (key, value + '\r\n' + line.rstrip())
- continue
-
- key, value = line.split(':', 1)
- headers.append((key, value.strip()))
-
- return cls(headers)
+from __future__ import absolute_import
+try:
+ from collections.abc import Mapping, MutableMapping
+except ImportError:
+ from collections import Mapping, MutableMapping
+try:
+ from threading import RLock
+except ImportError: # Platform-specific: No threads available
+ class RLock:
+ def __enter__(self):
+ pass
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ pass
+
+
+try: # Python 2.7+
+ from collections import OrderedDict
+except ImportError:
+ from .packages.ordered_dict import OrderedDict
+from .packages.six import iterkeys, itervalues, PY3
+
+
+__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
+
+
+_Null = object()
+
+
+class RecentlyUsedContainer(MutableMapping):
+ """
+ Provides a thread-safe dict-like container which maintains up to
+ ``maxsize`` keys while throwing away the least-recently-used keys beyond
+ ``maxsize``.
+
+ :param maxsize:
+ Maximum number of recent elements to retain.
+
+ :param dispose_func:
+ Every time an item is evicted from the container,
+ ``dispose_func(value)`` is called. Callback which will get called
+ """
+
+ ContainerCls = OrderedDict
+
+ def __init__(self, maxsize=10, dispose_func=None):
+ self._maxsize = maxsize
+ self.dispose_func = dispose_func
+
+ self._container = self.ContainerCls()
+ self.lock = RLock()
+
+ def __getitem__(self, key):
+ # Re-insert the item, moving it to the end of the eviction line.
+ with self.lock:
+ item = self._container.pop(key)
+ self._container[key] = item
+ return item
+
+ def __setitem__(self, key, value):
+ evicted_value = _Null
+ with self.lock:
+ # Possibly evict the existing value of 'key'
+ evicted_value = self._container.get(key, _Null)
+ self._container[key] = value
+
+ # If we didn't evict an existing value, we might have to evict the
+ # least recently used item from the beginning of the container.
+ if len(self._container) > self._maxsize:
+ _key, evicted_value = self._container.popitem(last=False)
+
+ if self.dispose_func and evicted_value is not _Null:
+ self.dispose_func(evicted_value)
+
+ def __delitem__(self, key):
+ with self.lock:
+ value = self._container.pop(key)
+
+ if self.dispose_func:
+ self.dispose_func(value)
+
+ def __len__(self):
+ with self.lock:
+ return len(self._container)
+
+ def __iter__(self):
+ raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.')
+
+ def clear(self):
+ with self.lock:
+ # Copy pointers to all values, then wipe the mapping
+ values = list(itervalues(self._container))
+ self._container.clear()
+
+ if self.dispose_func:
+ for value in values:
+ self.dispose_func(value)
+
+ def keys(self):
+ with self.lock:
+ return list(iterkeys(self._container))
+
+
+class HTTPHeaderDict(MutableMapping):
+ """
+ :param headers:
+ An iterable of field-value pairs. Must not contain multiple field names
+ when compared case-insensitively.
+
+ :param kwargs:
+ Additional field-value pairs to pass in to ``dict.update``.
+
+ A ``dict`` like container for storing HTTP Headers.
+
+ Field names are stored and compared case-insensitively in compliance with
+ RFC 7230. Iteration provides the first case-sensitive key seen for each
+ case-insensitive pair.
+
+ Using ``__setitem__`` syntax overwrites fields that compare equal
+ case-insensitively in order to maintain ``dict``'s api. For fields that
+ compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
+ in a loop.
+
+ If multiple fields that are equal case-insensitively are passed to the
+ constructor or ``.update``, the behavior is undefined and some will be
+ lost.
+
+ >>> headers = HTTPHeaderDict()
+ >>> headers.add('Set-Cookie', 'foo=bar')
+ >>> headers.add('set-cookie', 'baz=quxx')
+ >>> headers['content-length'] = '7'
+ >>> headers['SET-cookie']
+ 'foo=bar, baz=quxx'
+ >>> headers['Content-Length']
+ '7'
+ """
+
+ def __init__(self, headers=None, **kwargs):
+ super(HTTPHeaderDict, self).__init__()
+ self._container = OrderedDict()
+ if headers is not None:
+ if isinstance(headers, HTTPHeaderDict):
+ self._copy_from(headers)
+ else:
+ self.extend(headers)
+ if kwargs:
+ self.extend(kwargs)
+
+ def __setitem__(self, key, val):
+ self._container[key.lower()] = (key, val)
+ return self._container[key.lower()]
+
+ def __getitem__(self, key):
+ val = self._container[key.lower()]
+ return ', '.join(val[1:])
+
+ def __delitem__(self, key):
+ del self._container[key.lower()]
+
+ def __contains__(self, key):
+ return key.lower() in self._container
+
+ def __eq__(self, other):
+ if not isinstance(other, Mapping) and not hasattr(other, 'keys'):
+ return False
+ if not isinstance(other, type(self)):
+ other = type(self)(other)
+ return (dict((k.lower(), v) for k, v in self.itermerged()) ==
+ dict((k.lower(), v) for k, v in other.itermerged()))
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ if not PY3: # Python 2
+ iterkeys = MutableMapping.iterkeys
+ itervalues = MutableMapping.itervalues
+
+ __marker = object()
+
+ def __len__(self):
+ return len(self._container)
+
+ def __iter__(self):
+ # Only provide the originally cased names
+ for vals in self._container.values():
+ yield vals[0]
+
+ def pop(self, key, default=__marker):
+ '''D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
+ If key is not found, d is returned if given, otherwise KeyError is raised.
+ '''
+ # Using the MutableMapping function directly fails due to the private marker.
+ # Using ordinary dict.pop would expose the internal structures.
+ # So let's reinvent the wheel.
+ try:
+ value = self[key]
+ except KeyError:
+ if default is self.__marker:
+ raise
+ return default
+ else:
+ del self[key]
+ return value
+
+ def discard(self, key):
+ try:
+ del self[key]
+ except KeyError:
+ pass
+
+ def add(self, key, val):
+ """Adds a (name, value) pair, doesn't overwrite the value if it already
+ exists.
+
+ >>> headers = HTTPHeaderDict(foo='bar')
+ >>> headers.add('Foo', 'baz')
+ >>> headers['foo']
+ 'bar, baz'
+ """
+ key_lower = key.lower()
+ new_vals = key, val
+ # Keep the common case aka no item present as fast as possible
+ vals = self._container.setdefault(key_lower, new_vals)
+ if new_vals is not vals:
+ # new_vals was not inserted, as there was a previous one
+ if isinstance(vals, list):
+ # If already several items got inserted, we have a list
+ vals.append(val)
+ else:
+ # vals should be a tuple then, i.e. only one item so far
+ # Need to convert the tuple to list for further extension
+ self._container[key_lower] = [vals[0], vals[1], val]
+
+ def extend(self, *args, **kwargs):
+ """Generic import function for any type of header-like object.
+ Adapted version of MutableMapping.update in order to insert items
+ with self.add instead of self.__setitem__
+ """
+ if len(args) > 1:
+ raise TypeError("extend() takes at most 1 positional "
+ "arguments ({0} given)".format(len(args)))
+ other = args[0] if len(args) >= 1 else ()
+
+ if isinstance(other, HTTPHeaderDict):
+ for key, val in other.iteritems():
+ self.add(key, val)
+ elif isinstance(other, Mapping):
+ for key in other:
+ self.add(key, other[key])
+ elif hasattr(other, "keys"):
+ for key in other.keys():
+ self.add(key, other[key])
+ else:
+ for key, value in other:
+ self.add(key, value)
+
+ for key, value in kwargs.items():
+ self.add(key, value)
+
+ def getlist(self, key):
+ """Returns a list of all the values for the named field. Returns an
+ empty list if the key doesn't exist."""
+ try:
+ vals = self._container[key.lower()]
+ except KeyError:
+ return []
+ else:
+ if isinstance(vals, tuple):
+ return [vals[1]]
+ else:
+ return vals[1:]
+
+ # Backwards compatibility for httplib
+ getheaders = getlist
+ getallmatchingheaders = getlist
+ iget = getlist
+
+ def __repr__(self):
+ return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
+
+ def _copy_from(self, other):
+ for key in other:
+ val = other.getlist(key)
+ if isinstance(val, list):
+ # Don't need to convert tuples
+ val = list(val)
+ self._container[key.lower()] = [key] + val
+
+ def copy(self):
+ clone = type(self)()
+ clone._copy_from(self)
+ return clone
+
+ def iteritems(self):
+ """Iterate over all header lines, including duplicate ones."""
+ for key in self:
+ vals = self._container[key.lower()]
+ for val in vals[1:]:
+ yield vals[0], val
+
+ def itermerged(self):
+ """Iterate over all headers, merging duplicate ones together."""
+ for key in self:
+ val = self._container[key.lower()]
+ yield val[0], ', '.join(val[1:])
+
+ def items(self):
+ return list(self.iteritems())
+
+ @classmethod
+ def from_httplib(cls, message): # Python 2
+ """Read headers from a Python 2 httplib message object."""
+ # python2.7 does not expose a proper API for exporting multiheaders
+ # efficiently. This function re-reads raw lines from the message
+ # object and extracts the multiheaders properly.
+ headers = []
+
+ for line in message.headers:
+ if line.startswith((' ', '\t')):
+ key, value = headers[-1]
+ headers[-1] = (key, value + '\r\n' + line.rstrip())
+ continue
+
+ key, value = line.split(':', 1)
+ headers.append((key, value.strip()))
+
+ return cls(headers)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connection.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connection.py
index e05ae0b..9f06c39 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connection.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connection.py
@@ -1,369 +1,369 @@
-from __future__ import absolute_import
-import datetime
-import logging
-import os
-import sys
-import socket
-from socket import error as SocketError, timeout as SocketTimeout
-import warnings
-from .packages import six
-from .packages.six.moves.http_client import HTTPConnection as _HTTPConnection
-from .packages.six.moves.http_client import HTTPException # noqa: F401
-
-try: # Compiled with SSL?
- import ssl
- BaseSSLError = ssl.SSLError
-except (ImportError, AttributeError): # Platform-specific: No SSL.
- ssl = None
-
- class BaseSSLError(BaseException):
- pass
-
-
-try: # Python 3:
- # Not a no-op, we're adding this to the namespace so it can be imported.
- ConnectionError = ConnectionError
-except NameError: # Python 2:
- class ConnectionError(Exception):
- pass
-
-
-from .exceptions import (
- NewConnectionError,
- ConnectTimeoutError,
- SubjectAltNameWarning,
- SystemTimeWarning,
-)
-from .packages.ssl_match_hostname import match_hostname, CertificateError
-
-from .util.ssl_ import (
- resolve_cert_reqs,
- resolve_ssl_version,
- assert_fingerprint,
- create_urllib3_context,
- ssl_wrap_socket
-)
-
-
-from .util import connection
-
-from ._collections import HTTPHeaderDict
-
-log = logging.getLogger(__name__)
-
-port_by_scheme = {
- 'http': 80,
- 'https': 443,
-}
-
-# When updating RECENT_DATE, move it to
-# within two years of the current date, and no
-# earlier than 6 months ago.
-RECENT_DATE = datetime.date(2016, 1, 1)
-
-
-class DummyConnection(object):
- """Used to detect a failed ConnectionCls import."""
- pass
-
-
-class HTTPConnection(_HTTPConnection, object):
- """
- Based on httplib.HTTPConnection but provides an extra constructor
- backwards-compatibility layer between older and newer Pythons.
-
- Additional keyword parameters are used to configure attributes of the connection.
- Accepted parameters include:
-
- - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool`
- - ``source_address``: Set the source address for the current connection.
-
- .. note:: This is ignored for Python 2.6. It is only applied for 2.7 and 3.x
-
- - ``socket_options``: Set specific options on the underlying socket. If not specified, then
- defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling
- Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy.
-
- For example, if you wish to enable TCP Keep Alive in addition to the defaults,
- you might pass::
-
- HTTPConnection.default_socket_options + [
- (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
- ]
-
- Or you may want to disable the defaults by passing an empty list (e.g., ``[]``).
- """
-
- default_port = port_by_scheme['http']
-
- #: Disable Nagle's algorithm by default.
- #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]``
- default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]
-
- #: Whether this connection verifies the host's certificate.
- is_verified = False
-
- def __init__(self, *args, **kw):
- if six.PY3: # Python 3
- kw.pop('strict', None)
-
- # Pre-set source_address in case we have an older Python like 2.6.
- self.source_address = kw.get('source_address')
-
- if sys.version_info < (2, 7): # Python 2.6
- # _HTTPConnection on Python 2.6 will balk at this keyword arg, but
- # not newer versions. We can still use it when creating a
- # connection though, so we pop it *after* we have saved it as
- # self.source_address.
- kw.pop('source_address', None)
-
- #: The socket options provided by the user. If no options are
- #: provided, we use the default options.
- self.socket_options = kw.pop('socket_options', self.default_socket_options)
-
- # Superclass also sets self.source_address in Python 2.7+.
- _HTTPConnection.__init__(self, *args, **kw)
-
- def _new_conn(self):
- """ Establish a socket connection and set nodelay settings on it.
-
- :return: New socket connection.
- """
- extra_kw = {}
- if self.source_address:
- extra_kw['source_address'] = self.source_address
-
- if self.socket_options:
- extra_kw['socket_options'] = self.socket_options
-
- try:
- conn = connection.create_connection(
- (self.host, self.port), self.timeout, **extra_kw)
-
- except SocketTimeout as e:
- raise ConnectTimeoutError(
- self, "Connection to %s timed out. (connect timeout=%s)" %
- (self.host, self.timeout))
-
- except SocketError as e:
- raise NewConnectionError(
- self, "Failed to establish a new connection: %s" % e)
-
- return conn
-
- def _prepare_conn(self, conn):
- self.sock = conn
- # the _tunnel_host attribute was added in python 2.6.3 (via
- # http://hg.python.org/cpython/rev/0f57b30a152f) so pythons 2.6(0-2) do
- # not have them.
- if getattr(self, '_tunnel_host', None):
- # TODO: Fix tunnel so it doesn't depend on self.sock state.
- self._tunnel()
- # Mark this connection as not reusable
- self.auto_open = 0
-
- def connect(self):
- conn = self._new_conn()
- self._prepare_conn(conn)
-
- def request_chunked(self, method, url, body=None, headers=None):
- """
- Alternative to the common request method, which sends the
- body with chunked encoding and not as one block
- """
- headers = HTTPHeaderDict(headers if headers is not None else {})
- skip_accept_encoding = 'accept-encoding' in headers
- skip_host = 'host' in headers
- self.putrequest(
- method,
- url,
- skip_accept_encoding=skip_accept_encoding,
- skip_host=skip_host
- )
- for header, value in headers.items():
- self.putheader(header, value)
- if 'transfer-encoding' not in headers:
- self.putheader('Transfer-Encoding', 'chunked')
- self.endheaders()
-
- if body is not None:
- stringish_types = six.string_types + (six.binary_type,)
- if isinstance(body, stringish_types):
- body = (body,)
- for chunk in body:
- if not chunk:
- continue
- if not isinstance(chunk, six.binary_type):
- chunk = chunk.encode('utf8')
- len_str = hex(len(chunk))[2:]
- self.send(len_str.encode('utf-8'))
- self.send(b'\r\n')
- self.send(chunk)
- self.send(b'\r\n')
-
- # After the if clause, to always have a closed body
- self.send(b'0\r\n\r\n')
-
-
-class HTTPSConnection(HTTPConnection):
- default_port = port_by_scheme['https']
-
- ssl_version = None
-
- def __init__(self, host, port=None, key_file=None, cert_file=None,
- strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
- ssl_context=None, **kw):
-
- HTTPConnection.__init__(self, host, port, strict=strict,
- timeout=timeout, **kw)
-
- self.key_file = key_file
- self.cert_file = cert_file
- self.ssl_context = ssl_context
-
- # Required property for Google AppEngine 1.9.0 which otherwise causes
- # HTTPS requests to go out as HTTP. (See Issue #356)
- self._protocol = 'https'
-
- def connect(self):
- conn = self._new_conn()
- self._prepare_conn(conn)
-
- if self.ssl_context is None:
- self.ssl_context = create_urllib3_context(
- ssl_version=resolve_ssl_version(None),
- cert_reqs=resolve_cert_reqs(None),
- )
-
- self.sock = ssl_wrap_socket(
- sock=conn,
- keyfile=self.key_file,
- certfile=self.cert_file,
- ssl_context=self.ssl_context,
- )
-
-
-class VerifiedHTTPSConnection(HTTPSConnection):
- """
- Based on httplib.HTTPSConnection but wraps the socket with
- SSL certification.
- """
- cert_reqs = None
- ca_certs = None
- ca_cert_dir = None
- ssl_version = None
- assert_fingerprint = None
-
- def set_cert(self, key_file=None, cert_file=None,
- cert_reqs=None, ca_certs=None,
- assert_hostname=None, assert_fingerprint=None,
- ca_cert_dir=None):
- """
- This method should only be called once, before the connection is used.
- """
- # If cert_reqs is not provided, we can try to guess. If the user gave
- # us a cert database, we assume they want to use it: otherwise, if
- # they gave us an SSL Context object we should use whatever is set for
- # it.
- if cert_reqs is None:
- if ca_certs or ca_cert_dir:
- cert_reqs = 'CERT_REQUIRED'
- elif self.ssl_context is not None:
- cert_reqs = self.ssl_context.verify_mode
-
- self.key_file = key_file
- self.cert_file = cert_file
- self.cert_reqs = cert_reqs
- self.assert_hostname = assert_hostname
- self.assert_fingerprint = assert_fingerprint
- self.ca_certs = ca_certs and os.path.expanduser(ca_certs)
- self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir)
-
- def connect(self):
- # Add certificate verification
- conn = self._new_conn()
-
- hostname = self.host
- if getattr(self, '_tunnel_host', None):
- # _tunnel_host was added in Python 2.6.3
- # (See: http://hg.python.org/cpython/rev/0f57b30a152f)
-
- self.sock = conn
- # Calls self._set_hostport(), so self.host is
- # self._tunnel_host below.
- self._tunnel()
- # Mark this connection as not reusable
- self.auto_open = 0
-
- # Override the host with the one we're requesting data from.
- hostname = self._tunnel_host
-
- is_time_off = datetime.date.today() < RECENT_DATE
- if is_time_off:
- warnings.warn((
- 'System time is way off (before {0}). This will probably '
- 'lead to SSL verification errors').format(RECENT_DATE),
- SystemTimeWarning
- )
-
- # Wrap socket using verification with the root certs in
- # trusted_root_certs
- if self.ssl_context is None:
- self.ssl_context = create_urllib3_context(
- ssl_version=resolve_ssl_version(self.ssl_version),
- cert_reqs=resolve_cert_reqs(self.cert_reqs),
- )
-
- context = self.ssl_context
- context.verify_mode = resolve_cert_reqs(self.cert_reqs)
- self.sock = ssl_wrap_socket(
- sock=conn,
- keyfile=self.key_file,
- certfile=self.cert_file,
- ca_certs=self.ca_certs,
- ca_cert_dir=self.ca_cert_dir,
- server_hostname=hostname,
- ssl_context=context)
-
- if self.assert_fingerprint:
- assert_fingerprint(self.sock.getpeercert(binary_form=True),
- self.assert_fingerprint)
- elif context.verify_mode != ssl.CERT_NONE \
- and self.assert_hostname is not False:
- cert = self.sock.getpeercert()
- if not cert.get('subjectAltName', ()):
- warnings.warn((
- 'Certificate for {0} has no `subjectAltName`, falling back to check for a '
- '`commonName` for now. This feature is being removed by major browsers and '
- 'deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 '
- 'for details.)'.format(hostname)),
- SubjectAltNameWarning
- )
- _match_hostname(cert, self.assert_hostname or hostname)
-
- self.is_verified = (
- context.verify_mode == ssl.CERT_REQUIRED or
- self.assert_fingerprint is not None
- )
-
-
-def _match_hostname(cert, asserted_hostname):
- try:
- match_hostname(cert, asserted_hostname)
- except CertificateError as e:
- log.error(
- 'Certificate did not match expected hostname: %s. '
- 'Certificate: %s', asserted_hostname, cert
- )
- # Add cert to exception and reraise so client code can inspect
- # the cert when catching the exception, if they want to
- e._peer_cert = cert
- raise
-
-
-if ssl:
- # Make a copy for testing.
- UnverifiedHTTPSConnection = HTTPSConnection
- HTTPSConnection = VerifiedHTTPSConnection
-else:
- HTTPSConnection = DummyConnection
+from __future__ import absolute_import
+import datetime
+import logging
+import os
+import sys
+import socket
+from socket import error as SocketError, timeout as SocketTimeout
+import warnings
+from .packages import six
+from .packages.six.moves.http_client import HTTPConnection as _HTTPConnection
+from .packages.six.moves.http_client import HTTPException # noqa: F401
+
+try: # Compiled with SSL?
+ import ssl
+ BaseSSLError = ssl.SSLError
+except (ImportError, AttributeError): # Platform-specific: No SSL.
+ ssl = None
+
+ class BaseSSLError(BaseException):
+ pass
+
+
+try: # Python 3:
+ # Not a no-op, we're adding this to the namespace so it can be imported.
+ ConnectionError = ConnectionError
+except NameError: # Python 2:
+ class ConnectionError(Exception):
+ pass
+
+
+from .exceptions import (
+ NewConnectionError,
+ ConnectTimeoutError,
+ SubjectAltNameWarning,
+ SystemTimeWarning,
+)
+from .packages.ssl_match_hostname import match_hostname, CertificateError
+
+from .util.ssl_ import (
+ resolve_cert_reqs,
+ resolve_ssl_version,
+ assert_fingerprint,
+ create_urllib3_context,
+ ssl_wrap_socket
+)
+
+
+from .util import connection
+
+from ._collections import HTTPHeaderDict
+
+log = logging.getLogger(__name__)
+
+port_by_scheme = {
+ 'http': 80,
+ 'https': 443,
+}
+
+# When updating RECENT_DATE, move it to
+# within two years of the current date, and no
+# earlier than 6 months ago.
+RECENT_DATE = datetime.date(2016, 1, 1)
+
+
+class DummyConnection(object):
+ """Used to detect a failed ConnectionCls import."""
+ pass
+
+
+class HTTPConnection(_HTTPConnection, object):
+ """
+ Based on httplib.HTTPConnection but provides an extra constructor
+ backwards-compatibility layer between older and newer Pythons.
+
+ Additional keyword parameters are used to configure attributes of the connection.
+ Accepted parameters include:
+
+ - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool`
+ - ``source_address``: Set the source address for the current connection.
+
+ .. note:: This is ignored for Python 2.6. It is only applied for 2.7 and 3.x
+
+ - ``socket_options``: Set specific options on the underlying socket. If not specified, then
+ defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling
+ Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy.
+
+ For example, if you wish to enable TCP Keep Alive in addition to the defaults,
+ you might pass::
+
+ HTTPConnection.default_socket_options + [
+ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
+ ]
+
+ Or you may want to disable the defaults by passing an empty list (e.g., ``[]``).
+ """
+
+ default_port = port_by_scheme['http']
+
+ #: Disable Nagle's algorithm by default.
+ #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]``
+ default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]
+
+ #: Whether this connection verifies the host's certificate.
+ is_verified = False
+
+ def __init__(self, *args, **kw):
+ if six.PY3: # Python 3
+ kw.pop('strict', None)
+
+ # Pre-set source_address in case we have an older Python like 2.6.
+ self.source_address = kw.get('source_address')
+
+ if sys.version_info < (2, 7): # Python 2.6
+ # _HTTPConnection on Python 2.6 will balk at this keyword arg, but
+ # not newer versions. We can still use it when creating a
+ # connection though, so we pop it *after* we have saved it as
+ # self.source_address.
+ kw.pop('source_address', None)
+
+ #: The socket options provided by the user. If no options are
+ #: provided, we use the default options.
+ self.socket_options = kw.pop('socket_options', self.default_socket_options)
+
+ # Superclass also sets self.source_address in Python 2.7+.
+ _HTTPConnection.__init__(self, *args, **kw)
+
+ def _new_conn(self):
+ """ Establish a socket connection and set nodelay settings on it.
+
+ :return: New socket connection.
+ """
+ extra_kw = {}
+ if self.source_address:
+ extra_kw['source_address'] = self.source_address
+
+ if self.socket_options:
+ extra_kw['socket_options'] = self.socket_options
+
+ try:
+ conn = connection.create_connection(
+ (self.host, self.port), self.timeout, **extra_kw)
+
+ except SocketTimeout as e:
+ raise ConnectTimeoutError(
+ self, "Connection to %s timed out. (connect timeout=%s)" %
+ (self.host, self.timeout))
+
+ except SocketError as e:
+ raise NewConnectionError(
+ self, "Failed to establish a new connection: %s" % e)
+
+ return conn
+
+ def _prepare_conn(self, conn):
+ self.sock = conn
+ # the _tunnel_host attribute was added in python 2.6.3 (via
+ # http://hg.python.org/cpython/rev/0f57b30a152f) so pythons 2.6(0-2) do
+ # not have them.
+ if getattr(self, '_tunnel_host', None):
+ # TODO: Fix tunnel so it doesn't depend on self.sock state.
+ self._tunnel()
+ # Mark this connection as not reusable
+ self.auto_open = 0
+
+ def connect(self):
+ conn = self._new_conn()
+ self._prepare_conn(conn)
+
+ def request_chunked(self, method, url, body=None, headers=None):
+ """
+ Alternative to the common request method, which sends the
+ body with chunked encoding and not as one block
+ """
+ headers = HTTPHeaderDict(headers if headers is not None else {})
+ skip_accept_encoding = 'accept-encoding' in headers
+ skip_host = 'host' in headers
+ self.putrequest(
+ method,
+ url,
+ skip_accept_encoding=skip_accept_encoding,
+ skip_host=skip_host
+ )
+ for header, value in headers.items():
+ self.putheader(header, value)
+ if 'transfer-encoding' not in headers:
+ self.putheader('Transfer-Encoding', 'chunked')
+ self.endheaders()
+
+ if body is not None:
+ stringish_types = six.string_types + (six.binary_type,)
+ if isinstance(body, stringish_types):
+ body = (body,)
+ for chunk in body:
+ if not chunk:
+ continue
+ if not isinstance(chunk, six.binary_type):
+ chunk = chunk.encode('utf8')
+ len_str = hex(len(chunk))[2:]
+ self.send(len_str.encode('utf-8'))
+ self.send(b'\r\n')
+ self.send(chunk)
+ self.send(b'\r\n')
+
+ # After the if clause, to always have a closed body
+ self.send(b'0\r\n\r\n')
+
+
+class HTTPSConnection(HTTPConnection):
+ default_port = port_by_scheme['https']
+
+ ssl_version = None
+
+ def __init__(self, host, port=None, key_file=None, cert_file=None,
+ strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
+ ssl_context=None, **kw):
+
+ HTTPConnection.__init__(self, host, port, strict=strict,
+ timeout=timeout, **kw)
+
+ self.key_file = key_file
+ self.cert_file = cert_file
+ self.ssl_context = ssl_context
+
+ # Required property for Google AppEngine 1.9.0 which otherwise causes
+ # HTTPS requests to go out as HTTP. (See Issue #356)
+ self._protocol = 'https'
+
+ def connect(self):
+ conn = self._new_conn()
+ self._prepare_conn(conn)
+
+ if self.ssl_context is None:
+ self.ssl_context = create_urllib3_context(
+ ssl_version=resolve_ssl_version(None),
+ cert_reqs=resolve_cert_reqs(None),
+ )
+
+ self.sock = ssl_wrap_socket(
+ sock=conn,
+ keyfile=self.key_file,
+ certfile=self.cert_file,
+ ssl_context=self.ssl_context,
+ )
+
+
+class VerifiedHTTPSConnection(HTTPSConnection):
+ """
+ Based on httplib.HTTPSConnection but wraps the socket with
+ SSL certification.
+ """
+ cert_reqs = None
+ ca_certs = None
+ ca_cert_dir = None
+ ssl_version = None
+ assert_fingerprint = None
+
+ def set_cert(self, key_file=None, cert_file=None,
+ cert_reqs=None, ca_certs=None,
+ assert_hostname=None, assert_fingerprint=None,
+ ca_cert_dir=None):
+ """
+ This method should only be called once, before the connection is used.
+ """
+ # If cert_reqs is not provided, we can try to guess. If the user gave
+ # us a cert database, we assume they want to use it: otherwise, if
+ # they gave us an SSL Context object we should use whatever is set for
+ # it.
+ if cert_reqs is None:
+ if ca_certs or ca_cert_dir:
+ cert_reqs = 'CERT_REQUIRED'
+ elif self.ssl_context is not None:
+ cert_reqs = self.ssl_context.verify_mode
+
+ self.key_file = key_file
+ self.cert_file = cert_file
+ self.cert_reqs = cert_reqs
+ self.assert_hostname = assert_hostname
+ self.assert_fingerprint = assert_fingerprint
+ self.ca_certs = ca_certs and os.path.expanduser(ca_certs)
+ self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir)
+
+ def connect(self):
+ # Add certificate verification
+ conn = self._new_conn()
+
+ hostname = self.host
+ if getattr(self, '_tunnel_host', None):
+ # _tunnel_host was added in Python 2.6.3
+ # (See: http://hg.python.org/cpython/rev/0f57b30a152f)
+
+ self.sock = conn
+ # Calls self._set_hostport(), so self.host is
+ # self._tunnel_host below.
+ self._tunnel()
+ # Mark this connection as not reusable
+ self.auto_open = 0
+
+ # Override the host with the one we're requesting data from.
+ hostname = self._tunnel_host
+
+ is_time_off = datetime.date.today() < RECENT_DATE
+ if is_time_off:
+ warnings.warn((
+ 'System time is way off (before {0}). This will probably '
+ 'lead to SSL verification errors').format(RECENT_DATE),
+ SystemTimeWarning
+ )
+
+ # Wrap socket using verification with the root certs in
+ # trusted_root_certs
+ if self.ssl_context is None:
+ self.ssl_context = create_urllib3_context(
+ ssl_version=resolve_ssl_version(self.ssl_version),
+ cert_reqs=resolve_cert_reqs(self.cert_reqs),
+ )
+
+ context = self.ssl_context
+ context.verify_mode = resolve_cert_reqs(self.cert_reqs)
+ self.sock = ssl_wrap_socket(
+ sock=conn,
+ keyfile=self.key_file,
+ certfile=self.cert_file,
+ ca_certs=self.ca_certs,
+ ca_cert_dir=self.ca_cert_dir,
+ server_hostname=hostname,
+ ssl_context=context)
+
+ if self.assert_fingerprint:
+ assert_fingerprint(self.sock.getpeercert(binary_form=True),
+ self.assert_fingerprint)
+ elif context.verify_mode != ssl.CERT_NONE \
+ and self.assert_hostname is not False:
+ cert = self.sock.getpeercert()
+ if not cert.get('subjectAltName', ()):
+ warnings.warn((
+ 'Certificate for {0} has no `subjectAltName`, falling back to check for a '
+ '`commonName` for now. This feature is being removed by major browsers and '
+ 'deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 '
+ 'for details.)'.format(hostname)),
+ SubjectAltNameWarning
+ )
+ _match_hostname(cert, self.assert_hostname or hostname)
+
+ self.is_verified = (
+ context.verify_mode == ssl.CERT_REQUIRED or
+ self.assert_fingerprint is not None
+ )
+
+
+def _match_hostname(cert, asserted_hostname):
+ try:
+ match_hostname(cert, asserted_hostname)
+ except CertificateError as e:
+ log.error(
+ 'Certificate did not match expected hostname: %s. '
+ 'Certificate: %s', asserted_hostname, cert
+ )
+ # Add cert to exception and reraise so client code can inspect
+ # the cert when catching the exception, if they want to
+ e._peer_cert = cert
+ raise
+
+
+if ssl:
+ # Make a copy for testing.
+ UnverifiedHTTPSConnection = HTTPSConnection
+ HTTPSConnection = VerifiedHTTPSConnection
+else:
+ HTTPSConnection = DummyConnection
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connectionpool.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connectionpool.py
index 1073a1f..a958f99 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connectionpool.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/connectionpool.py
@@ -1,912 +1,912 @@
-from __future__ import absolute_import
-import errno
-import logging
-import sys
-import warnings
-
-from socket import error as SocketError, timeout as SocketTimeout
-import socket
-
-
-from .exceptions import (
- ClosedPoolError,
- ProtocolError,
- EmptyPoolError,
- HeaderParsingError,
- HostChangedError,
- LocationValueError,
- MaxRetryError,
- ProxyError,
- ReadTimeoutError,
- SSLError,
- TimeoutError,
- InsecureRequestWarning,
- NewConnectionError,
- ConnectTimeoutError,
-)
-from .packages.ssl_match_hostname import CertificateError
-from .packages import six
-from .packages.six.moves import queue
-from .connection import (
- port_by_scheme,
- DummyConnection,
- HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection,
- HTTPException, BaseSSLError,
-)
-from .request import RequestMethods
-from .response import HTTPResponse
-
-from .util.connection import is_connection_dropped
-from .util.request import set_file_position
-from .util.response import assert_header_parsing
-from .util.retry import Retry
-from .util.timeout import Timeout
-from .util.url import get_host, Url
-
-
-if six.PY2:
- # Queue is imported for side effects on MS Windows
- import Queue as _unused_module_Queue # noqa: F401
-
-xrange = six.moves.xrange
-
-log = logging.getLogger(__name__)
-
-_Default = object()
-
-
-# Pool objects
-class ConnectionPool(object):
- """
- Base class for all connection pools, such as
- :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`.
- """
-
- scheme = None
- QueueCls = queue.LifoQueue
-
- def __init__(self, host, port=None):
- if not host:
- raise LocationValueError("No host specified.")
-
- self.host = _ipv6_host(host).lower()
- self.port = port
-
- def __str__(self):
- return '%s(host=%r, port=%r)' % (type(self).__name__,
- self.host, self.port)
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.close()
- # Return False to re-raise any potential exceptions
- return False
-
- def close(self):
- """
- Close all pooled connections and disable the pool.
- """
- pass
-
-
-# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252
-_blocking_errnos = set([errno.EAGAIN, errno.EWOULDBLOCK])
-
-
-class HTTPConnectionPool(ConnectionPool, RequestMethods):
- """
- Thread-safe connection pool for one host.
-
- :param host:
- Host used for this HTTP Connection (e.g. "localhost"), passed into
- :class:`httplib.HTTPConnection`.
-
- :param port:
- Port used for this HTTP Connection (None is equivalent to 80), passed
- into :class:`httplib.HTTPConnection`.
-
- :param strict:
- Causes BadStatusLine to be raised if the status line can't be parsed
- as a valid HTTP/1.0 or 1.1 status line, passed into
- :class:`httplib.HTTPConnection`.
-
- .. note::
- Only works in Python 2. This parameter is ignored in Python 3.
-
- :param timeout:
- Socket timeout in seconds for each individual connection. This can
- be a float or integer, which sets the timeout for the HTTP request,
- or an instance of :class:`urllib3.util.Timeout` which gives you more
- fine-grained control over request timeouts. After the constructor has
- been parsed, this is always a `urllib3.util.Timeout` object.
-
- :param maxsize:
- Number of connections to save that can be reused. More than 1 is useful
- in multithreaded situations. If ``block`` is set to False, more
- connections will be created but they will not be saved once they've
- been used.
-
- :param block:
- If set to True, no more than ``maxsize`` connections will be used at
- a time. When no free connections are available, the call will block
- until a connection has been released. This is a useful side effect for
- particular multithreaded situations where one does not want to use more
- than maxsize connections per host to prevent flooding.
-
- :param headers:
- Headers to include with all requests, unless other headers are given
- explicitly.
-
- :param retries:
- Retry configuration to use by default with requests in this pool.
-
- :param _proxy:
- Parsed proxy URL, should not be used directly, instead, see
- :class:`urllib3.connectionpool.ProxyManager`"
-
- :param _proxy_headers:
- A dictionary with proxy headers, should not be used directly,
- instead, see :class:`urllib3.connectionpool.ProxyManager`"
-
- :param \\**conn_kw:
- Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`,
- :class:`urllib3.connection.HTTPSConnection` instances.
- """
-
- scheme = 'http'
- ConnectionCls = HTTPConnection
- ResponseCls = HTTPResponse
-
- def __init__(self, host, port=None, strict=False,
- timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False,
- headers=None, retries=None,
- _proxy=None, _proxy_headers=None,
- **conn_kw):
- ConnectionPool.__init__(self, host, port)
- RequestMethods.__init__(self, headers)
-
- self.strict = strict
-
- if not isinstance(timeout, Timeout):
- timeout = Timeout.from_float(timeout)
-
- if retries is None:
- retries = Retry.DEFAULT
-
- self.timeout = timeout
- self.retries = retries
-
- self.pool = self.QueueCls(maxsize)
- self.block = block
-
- self.proxy = _proxy
- self.proxy_headers = _proxy_headers or {}
-
- # Fill the queue up so that doing get() on it will block properly
- for _ in xrange(maxsize):
- self.pool.put(None)
-
- # These are mostly for testing and debugging purposes.
- self.num_connections = 0
- self.num_requests = 0
- self.conn_kw = conn_kw
-
- if self.proxy:
- # Enable Nagle's algorithm for proxies, to avoid packet fragmentation.
- # We cannot know if the user has added default socket options, so we cannot replace the
- # list.
- self.conn_kw.setdefault('socket_options', [])
-
- def _new_conn(self):
- """
- Return a fresh :class:`HTTPConnection`.
- """
- self.num_connections += 1
- log.debug("Starting new HTTP connection (%d): %s",
- self.num_connections, self.host)
-
- conn = self.ConnectionCls(host=self.host, port=self.port,
- timeout=self.timeout.connect_timeout,
- strict=self.strict, **self.conn_kw)
- return conn
-
- def _get_conn(self, timeout=None):
- """
- Get a connection. Will return a pooled connection if one is available.
-
- If no connections are available and :prop:`.block` is ``False``, then a
- fresh connection is returned.
-
- :param timeout:
- Seconds to wait before giving up and raising
- :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and
- :prop:`.block` is ``True``.
- """
- conn = None
- try:
- conn = self.pool.get(block=self.block, timeout=timeout)
-
- except AttributeError: # self.pool is None
- raise ClosedPoolError(self, "Pool is closed.")
-
- except queue.Empty:
- if self.block:
- raise EmptyPoolError(self,
- "Pool reached maximum size and no more "
- "connections are allowed.")
- pass # Oh well, we'll create a new connection then
-
- # If this is a persistent connection, check if it got disconnected
- if conn and is_connection_dropped(conn):
- log.debug("Resetting dropped connection: %s", self.host)
- conn.close()
- if getattr(conn, 'auto_open', 1) == 0:
- # This is a proxied connection that has been mutated by
- # httplib._tunnel() and cannot be reused (since it would
- # attempt to bypass the proxy)
- conn = None
-
- return conn or self._new_conn()
-
- def _put_conn(self, conn):
- """
- Put a connection back into the pool.
-
- :param conn:
- Connection object for the current host and port as returned by
- :meth:`._new_conn` or :meth:`._get_conn`.
-
- If the pool is already full, the connection is closed and discarded
- because we exceeded maxsize. If connections are discarded frequently,
- then maxsize should be increased.
-
- If the pool is closed, then the connection will be closed and discarded.
- """
- try:
- self.pool.put(conn, block=False)
- return # Everything is dandy, done.
- except AttributeError:
- # self.pool is None.
- pass
- except queue.Full:
- # This should never happen if self.block == True
- log.warning(
- "Connection pool is full, discarding connection: %s",
- self.host)
-
- # Connection never got put back into the pool, close it.
- if conn:
- conn.close()
-
- def _validate_conn(self, conn):
- """
- Called right before a request is made, after the socket is created.
- """
- # Force connect early to allow us to set read timeout in time
- if not getattr(conn, 'sock', None): # AppEngine might not have `.sock`
- conn.connect()
-
- def _prepare_proxy(self, conn):
- # Nothing to do for HTTP connections.
- pass
-
- def _get_timeout(self, timeout):
- """ Helper that always returns a :class:`urllib3.util.Timeout` """
- if timeout is _Default:
- return self.timeout.clone()
-
- if isinstance(timeout, Timeout):
- return timeout.clone()
- else:
- # User passed us an int/float. This is for backwards compatibility,
- # can be removed later
- return Timeout.from_float(timeout)
-
- def _raise_timeout(self, err, url, timeout_value, exc_cls):
- """Is the error actually a timeout? Will raise a ReadTimeout or pass"""
-
- # exc_cls is either ReadTimeoutError or ConnectTimeoutError
- # Only ReadTimeoutError requires the url (preserving old behaviour)
- args = [self]
- if exc_cls is ReadTimeoutError:
- args.append(url)
- desc = 'Read'
- else:
- desc = 'Connect'
-
- if isinstance(err, SocketTimeout):
- args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value))
- raise exc_cls(*args)
-
- # See the above comment about EAGAIN in Python 3. In Python 2 we have
- # to specifically catch it and throw the timeout error
- elif hasattr(err, 'errno') and err.errno in _blocking_errnos:
- args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value))
- raise exc_cls(*args)
-
- # Catch possible read timeouts thrown as SSL errors. If not the
- # case, rethrow the original. We need to do this because of:
- # http://bugs.python.org/issue10272
- elif 'timed out' in str(err) or 'did not complete (read)' in str(err): # Python 2.6
- args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value))
- raise exc_cls(*args)
-
- def _make_request(self, conn, method, url, timeout=_Default, chunked=False,
- **httplib_request_kw):
- """
- Perform a request on a given urllib connection object taken from our
- pool.
-
- :param conn:
- a connection from one of our connection pools
-
- :param timeout:
- Socket timeout in seconds for the request. This can be a
- float or integer, which will set the same timeout value for
- the socket connect and the socket read, or an instance of
- :class:`urllib3.util.Timeout`, which gives you more fine-grained
- control over your timeouts.
- """
- self.num_requests += 1
-
- timeout_obj = self._get_timeout(timeout)
- timeout_obj.start_connect()
- conn.timeout = timeout_obj.connect_timeout
-
- # Trigger any extra validation we need to do.
- try:
- self._validate_conn(conn)
- except (SocketTimeout, BaseSSLError) as e:
- # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.
- self._raise_timeout(err=e, url=url, timeout_value=conn.timeout,
- exc_cls=ConnectTimeoutError)
- raise
-
- # Reset the timeout for the recv() on the socket
- read_timeout = timeout_obj.read_timeout
-
- # App Engine doesn't have a sock attr
- if getattr(conn, 'sock', None):
- # In Python 3 socket.py will catch EAGAIN and return None when you
- # try and read into the file pointer created by http.client, which
- # instead raises a BadStatusLine exception. Instead of catching
- # the exception and assuming all BadStatusLine exceptions are read
- # timeouts, check for a zero timeout before making the request.
- if read_timeout == 0:
- raise ReadTimeoutError(
- self, url, "Read timed out. (read timeout=%s)" % read_timeout)
- if read_timeout is Timeout.DEFAULT_TIMEOUT:
- conn.sock.settimeout(socket.getdefaulttimeout())
- else: # None or a value
- conn.sock.settimeout(read_timeout)
-
- # conn.request() calls httplib.*.request, not the method in
- # urllib3.request. It also calls makefile (recv) on the socket.
- if chunked:
- conn.request_chunked(method, url, **httplib_request_kw)
- else:
- conn.request(method, url, **httplib_request_kw)
-
- # Receive the response from the server
- try:
- try: # Python 2.7, use buffering of HTTP responses
- httplib_response = conn.getresponse(buffering=True)
- except TypeError: # Python 2.6 and older, Python 3
- try:
- httplib_response = conn.getresponse()
- except Exception as e:
- # Remove the TypeError from the exception chain in Python 3;
- # otherwise it looks like a programming error was the cause.
- six.raise_from(e, None)
- except (SocketTimeout, BaseSSLError, SocketError) as e:
- self._raise_timeout(err=e, url=url, timeout_value=read_timeout,
- exc_cls=ReadTimeoutError)
- raise
-
- # AppEngine doesn't have a version attr.
- http_version = getattr(conn, '_http_vsn_str', 'HTTP/?')
- log.debug("%s://%s:%s \"%s %s %s\" %s %s", self.scheme, self.host, self.port,
- method, url, http_version, httplib_response.status,
- httplib_response.length)
-
- try:
- assert_header_parsing(httplib_response.msg)
- except HeaderParsingError as hpe: # Platform-specific: Python 3
- log.warning(
- 'Failed to parse headers (url=%s): %s',
- self._absolute_url(url), hpe, exc_info=True)
-
- return httplib_response
-
- def _absolute_url(self, path):
- return Url(scheme=self.scheme, host=self.host, port=self.port, path=path).url
-
- def close(self):
- """
- Close all pooled connections and disable the pool.
- """
- # Disable access to the pool
- old_pool, self.pool = self.pool, None
-
- try:
- while True:
- conn = old_pool.get(block=False)
- if conn:
- conn.close()
-
- except queue.Empty:
- pass # Done.
-
- def is_same_host(self, url):
- """
- Check if the given ``url`` is a member of the same host as this
- connection pool.
- """
- if url.startswith('/'):
- return True
-
- # TODO: Add optional support for socket.gethostbyname checking.
- scheme, host, port = get_host(url)
-
- host = _ipv6_host(host).lower()
-
- # Use explicit default port for comparison when none is given
- if self.port and not port:
- port = port_by_scheme.get(scheme)
- elif not self.port and port == port_by_scheme.get(scheme):
- port = None
-
- return (scheme, host, port) == (self.scheme, self.host, self.port)
-
- def urlopen(self, method, url, body=None, headers=None, retries=None,
- redirect=True, assert_same_host=True, timeout=_Default,
- pool_timeout=None, release_conn=None, chunked=False,
- body_pos=None, **response_kw):
- """
- Get a connection from the pool and perform an HTTP request. This is the
- lowest level call for making a request, so you'll need to specify all
- the raw details.
-
- .. note::
-
- More commonly, it's appropriate to use a convenience method provided
- by :class:`.RequestMethods`, such as :meth:`request`.
-
- .. note::
-
- `release_conn` will only behave as expected if
- `preload_content=False` because we want to make
- `preload_content=False` the default behaviour someday soon without
- breaking backwards compatibility.
-
- :param method:
- HTTP request method (such as GET, POST, PUT, etc.)
-
- :param body:
- Data to send in the request body (useful for creating
- POST requests, see HTTPConnectionPool.post_url for
- more convenience).
-
- :param headers:
- Dictionary of custom headers to send, such as User-Agent,
- If-None-Match, etc. If None, pool headers are used. If provided,
- these headers completely replace any pool-specific headers.
-
- :param retries:
- Configure the number of retries to allow before raising a
- :class:`~urllib3.exceptions.MaxRetryError` exception.
-
- Pass ``None`` to retry until you receive a response. Pass a
- :class:`~urllib3.util.retry.Retry` object for fine-grained control
- over different types of retries.
- Pass an integer number to retry connection errors that many times,
- but no other types of errors. Pass zero to never retry.
-
- If ``False``, then retries are disabled and any exception is raised
- immediately. Also, instead of raising a MaxRetryError on redirects,
- the redirect response will be returned.
-
- :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int.
-
- :param redirect:
- If True, automatically handle redirects (status codes 301, 302,
- 303, 307, 308). Each redirect counts as a retry. Disabling retries
- will disable redirect, too.
-
- :param assert_same_host:
- If ``True``, will make sure that the host of the pool requests is
- consistent else will raise HostChangedError. When False, you can
- use the pool on an HTTP proxy and request foreign hosts.
-
- :param timeout:
- If specified, overrides the default timeout for this one
- request. It may be a float (in seconds) or an instance of
- :class:`urllib3.util.Timeout`.
-
- :param pool_timeout:
- If set and the pool is set to block=True, then this method will
- block for ``pool_timeout`` seconds and raise EmptyPoolError if no
- connection is available within the time period.
-
- :param release_conn:
- If False, then the urlopen call will not release the connection
- back into the pool once a response is received (but will release if
- you read the entire contents of the response such as when
- `preload_content=True`). This is useful if you're not preloading
- the response's content immediately. You will need to call
- ``r.release_conn()`` on the response ``r`` to return the connection
- back into the pool. If None, it takes the value of
- ``response_kw.get('preload_content', True)``.
-
- :param chunked:
- If True, urllib3 will send the body using chunked transfer
- encoding. Otherwise, urllib3 will send the body using the standard
- content-length form. Defaults to False.
-
- :param int body_pos:
- Position to seek to in file-like body in the event of a retry or
- redirect. Typically this won't need to be set because urllib3 will
- auto-populate the value when needed.
-
- :param \\**response_kw:
- Additional parameters are passed to
- :meth:`urllib3.response.HTTPResponse.from_httplib`
- """
- if headers is None:
- headers = self.headers
-
- if not isinstance(retries, Retry):
- retries = Retry.from_int(retries, redirect=redirect, default=self.retries)
-
- if release_conn is None:
- release_conn = response_kw.get('preload_content', True)
-
- # Check host
- if assert_same_host and not self.is_same_host(url):
- raise HostChangedError(self, url, retries)
-
- conn = None
-
- # Track whether `conn` needs to be released before
- # returning/raising/recursing. Update this variable if necessary, and
- # leave `release_conn` constant throughout the function. That way, if
- # the function recurses, the original value of `release_conn` will be
- # passed down into the recursive call, and its value will be respected.
- #
- # See issue #651 [1] for details.
- #
- # [1]
- release_this_conn = release_conn
-
- # Merge the proxy headers. Only do this in HTTP. We have to copy the
- # headers dict so we can safely change it without those changes being
- # reflected in anyone else's copy.
- if self.scheme == 'http':
- headers = headers.copy()
- headers.update(self.proxy_headers)
-
- # Must keep the exception bound to a separate variable or else Python 3
- # complains about UnboundLocalError.
- err = None
-
- # Keep track of whether we cleanly exited the except block. This
- # ensures we do proper cleanup in finally.
- clean_exit = False
-
- # Rewind body position, if needed. Record current position
- # for future rewinds in the event of a redirect/retry.
- body_pos = set_file_position(body, body_pos)
-
- try:
- # Request a connection from the queue.
- timeout_obj = self._get_timeout(timeout)
- conn = self._get_conn(timeout=pool_timeout)
-
- conn.timeout = timeout_obj.connect_timeout
-
- is_new_proxy_conn = self.proxy is not None and not getattr(conn, 'sock', None)
- if is_new_proxy_conn:
- self._prepare_proxy(conn)
-
- # Make the request on the httplib connection object.
- httplib_response = self._make_request(conn, method, url,
- timeout=timeout_obj,
- body=body, headers=headers,
- chunked=chunked)
-
- # If we're going to release the connection in ``finally:``, then
- # the response doesn't need to know about the connection. Otherwise
- # it will also try to release it and we'll have a double-release
- # mess.
- response_conn = conn if not release_conn else None
-
- # Pass method to Response for length checking
- response_kw['request_method'] = method
-
- # Import httplib's response into our own wrapper object
- response = self.ResponseCls.from_httplib(httplib_response,
- pool=self,
- connection=response_conn,
- retries=retries,
- **response_kw)
-
- # Everything went great!
- clean_exit = True
-
- except queue.Empty:
- # Timed out by queue.
- raise EmptyPoolError(self, "No pool connections are available.")
-
- except (BaseSSLError, CertificateError) as e:
- # Close the connection. If a connection is reused on which there
- # was a Certificate error, the next request will certainly raise
- # another Certificate error.
- clean_exit = False
- raise SSLError(e)
-
- except SSLError:
- # Treat SSLError separately from BaseSSLError to preserve
- # traceback.
- clean_exit = False
- raise
-
- except (TimeoutError, HTTPException, SocketError, ProtocolError) as e:
- # Discard the connection for these exceptions. It will be
- # be replaced during the next _get_conn() call.
- clean_exit = False
-
- if isinstance(e, (SocketError, NewConnectionError)) and self.proxy:
- e = ProxyError('Cannot connect to proxy.', e)
- elif isinstance(e, (SocketError, HTTPException)):
- e = ProtocolError('Connection aborted.', e)
-
- retries = retries.increment(method, url, error=e, _pool=self,
- _stacktrace=sys.exc_info()[2])
- retries.sleep()
-
- # Keep track of the error for the retry warning.
- err = e
-
- finally:
- if not clean_exit:
- # We hit some kind of exception, handled or otherwise. We need
- # to throw the connection away unless explicitly told not to.
- # Close the connection, set the variable to None, and make sure
- # we put the None back in the pool to avoid leaking it.
- conn = conn and conn.close()
- release_this_conn = True
-
- if release_this_conn:
- # Put the connection back to be reused. If the connection is
- # expired then it will be None, which will get replaced with a
- # fresh connection during _get_conn.
- self._put_conn(conn)
-
- if not conn:
- # Try again
- log.warning("Retrying (%r) after connection "
- "broken by '%r': %s", retries, err, url)
- return self.urlopen(method, url, body, headers, retries,
- redirect, assert_same_host,
- timeout=timeout, pool_timeout=pool_timeout,
- release_conn=release_conn, body_pos=body_pos,
- **response_kw)
-
- # Handle redirect?
- redirect_location = redirect and response.get_redirect_location()
- if redirect_location:
- if response.status == 303:
- method = 'GET'
-
- try:
- retries = retries.increment(method, url, response=response, _pool=self)
- except MaxRetryError:
- if retries.raise_on_redirect:
- # Release the connection for this response, since we're not
- # returning it to be released manually.
- response.release_conn()
- raise
- return response
-
- retries.sleep_for_retry(response)
- log.debug("Redirecting %s -> %s", url, redirect_location)
- return self.urlopen(
- method, redirect_location, body, headers,
- retries=retries, redirect=redirect,
- assert_same_host=assert_same_host,
- timeout=timeout, pool_timeout=pool_timeout,
- release_conn=release_conn, body_pos=body_pos,
- **response_kw)
-
- # Check if we should retry the HTTP response.
- has_retry_after = bool(response.getheader('Retry-After'))
- if retries.is_retry(method, response.status, has_retry_after):
- try:
- retries = retries.increment(method, url, response=response, _pool=self)
- except MaxRetryError:
- if retries.raise_on_status:
- # Release the connection for this response, since we're not
- # returning it to be released manually.
- response.release_conn()
- raise
- return response
- retries.sleep(response)
- log.debug("Retry: %s", url)
- return self.urlopen(
- method, url, body, headers,
- retries=retries, redirect=redirect,
- assert_same_host=assert_same_host,
- timeout=timeout, pool_timeout=pool_timeout,
- release_conn=release_conn,
- body_pos=body_pos, **response_kw)
-
- return response
-
-
-class HTTPSConnectionPool(HTTPConnectionPool):
- """
- Same as :class:`.HTTPConnectionPool`, but HTTPS.
-
- When Python is compiled with the :mod:`ssl` module, then
- :class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates,
- instead of :class:`.HTTPSConnection`.
-
- :class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``,
- ``assert_hostname`` and ``host`` in this order to verify connections.
- If ``assert_hostname`` is False, no verification is done.
-
- The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``,
- ``ca_cert_dir``, and ``ssl_version`` are only used if :mod:`ssl` is
- available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade
- the connection socket into an SSL socket.
- """
-
- scheme = 'https'
- ConnectionCls = HTTPSConnection
-
- def __init__(self, host, port=None,
- strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1,
- block=False, headers=None, retries=None,
- _proxy=None, _proxy_headers=None,
- key_file=None, cert_file=None, cert_reqs=None,
- ca_certs=None, ssl_version=None,
- assert_hostname=None, assert_fingerprint=None,
- ca_cert_dir=None, **conn_kw):
-
- HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize,
- block, headers, retries, _proxy, _proxy_headers,
- **conn_kw)
-
- if ca_certs and cert_reqs is None:
- cert_reqs = 'CERT_REQUIRED'
-
- self.key_file = key_file
- self.cert_file = cert_file
- self.cert_reqs = cert_reqs
- self.ca_certs = ca_certs
- self.ca_cert_dir = ca_cert_dir
- self.ssl_version = ssl_version
- self.assert_hostname = assert_hostname
- self.assert_fingerprint = assert_fingerprint
-
- def _prepare_conn(self, conn):
- """
- Prepare the ``connection`` for :meth:`urllib3.util.ssl_wrap_socket`
- and establish the tunnel if proxy is used.
- """
-
- if isinstance(conn, VerifiedHTTPSConnection):
- conn.set_cert(key_file=self.key_file,
- cert_file=self.cert_file,
- cert_reqs=self.cert_reqs,
- ca_certs=self.ca_certs,
- ca_cert_dir=self.ca_cert_dir,
- assert_hostname=self.assert_hostname,
- assert_fingerprint=self.assert_fingerprint)
- conn.ssl_version = self.ssl_version
- return conn
-
- def _prepare_proxy(self, conn):
- """
- Establish tunnel connection early, because otherwise httplib
- would improperly set Host: header to proxy's IP:port.
- """
- # Python 2.7+
- try:
- set_tunnel = conn.set_tunnel
- except AttributeError: # Platform-specific: Python 2.6
- set_tunnel = conn._set_tunnel
-
- if sys.version_info <= (2, 6, 4) and not self.proxy_headers: # Python 2.6.4 and older
- set_tunnel(self.host, self.port)
- else:
- set_tunnel(self.host, self.port, self.proxy_headers)
-
- conn.connect()
-
- def _new_conn(self):
- """
- Return a fresh :class:`httplib.HTTPSConnection`.
- """
- self.num_connections += 1
- log.debug("Starting new HTTPS connection (%d): %s",
- self.num_connections, self.host)
-
- if not self.ConnectionCls or self.ConnectionCls is DummyConnection:
- raise SSLError("Can't connect to HTTPS URL because the SSL "
- "module is not available.")
-
- actual_host = self.host
- actual_port = self.port
- if self.proxy is not None:
- actual_host = self.proxy.host
- actual_port = self.proxy.port
-
- conn = self.ConnectionCls(host=actual_host, port=actual_port,
- timeout=self.timeout.connect_timeout,
- strict=self.strict, **self.conn_kw)
-
- return self._prepare_conn(conn)
-
- def _validate_conn(self, conn):
- """
- Called right before a request is made, after the socket is created.
- """
- super(HTTPSConnectionPool, self)._validate_conn(conn)
-
- if not conn.is_verified:
- warnings.warn((
- 'Unverified HTTPS request is being made. '
- 'Adding certificate verification is strongly advised. See: '
- 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html'
- '#ssl-warnings'),
- InsecureRequestWarning)
-
-
-def connection_from_url(url, **kw):
- """
- Given a url, return an :class:`.ConnectionPool` instance of its host.
-
- This is a shortcut for not having to parse out the scheme, host, and port
- of the url before creating an :class:`.ConnectionPool` instance.
-
- :param url:
- Absolute URL string that must include the scheme. Port is optional.
-
- :param \\**kw:
- Passes additional parameters to the constructor of the appropriate
- :class:`.ConnectionPool`. Useful for specifying things like
- timeout, maxsize, headers, etc.
-
- Example::
-
- >>> conn = connection_from_url('http://google.com/')
- >>> r = conn.request('GET', '/')
- """
- scheme, host, port = get_host(url)
- port = port or port_by_scheme.get(scheme, 80)
- if scheme == 'https':
- return HTTPSConnectionPool(host, port=port, **kw)
- else:
- return HTTPConnectionPool(host, port=port, **kw)
-
-
-def _ipv6_host(host):
- """
- Process IPv6 address literals
- """
-
- # httplib doesn't like it when we include brackets in IPv6 addresses
- # Specifically, if we include brackets but also pass the port then
- # httplib crazily doubles up the square brackets on the Host header.
- # Instead, we need to make sure we never pass ``None`` as the port.
- # However, for backward compatibility reasons we can't actually
- # *assert* that. See http://bugs.python.org/issue28539
- #
- # Also if an IPv6 address literal has a zone identifier, the
- # percent sign might be URIencoded, convert it back into ASCII
- if host.startswith('[') and host.endswith(']'):
- host = host.replace('%25', '%').strip('[]')
- return host
+from __future__ import absolute_import
+import errno
+import logging
+import sys
+import warnings
+
+from socket import error as SocketError, timeout as SocketTimeout
+import socket
+
+
+from .exceptions import (
+ ClosedPoolError,
+ ProtocolError,
+ EmptyPoolError,
+ HeaderParsingError,
+ HostChangedError,
+ LocationValueError,
+ MaxRetryError,
+ ProxyError,
+ ReadTimeoutError,
+ SSLError,
+ TimeoutError,
+ InsecureRequestWarning,
+ NewConnectionError,
+ ConnectTimeoutError,
+)
+from .packages.ssl_match_hostname import CertificateError
+from .packages import six
+from .packages.six.moves import queue
+from .connection import (
+ port_by_scheme,
+ DummyConnection,
+ HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection,
+ HTTPException, BaseSSLError,
+)
+from .request import RequestMethods
+from .response import HTTPResponse
+
+from .util.connection import is_connection_dropped
+from .util.request import set_file_position
+from .util.response import assert_header_parsing
+from .util.retry import Retry
+from .util.timeout import Timeout
+from .util.url import get_host, Url
+
+
+if six.PY2:
+ # Queue is imported for side effects on MS Windows
+ import Queue as _unused_module_Queue # noqa: F401
+
+xrange = six.moves.xrange
+
+log = logging.getLogger(__name__)
+
+_Default = object()
+
+
+# Pool objects
+class ConnectionPool(object):
+ """
+ Base class for all connection pools, such as
+ :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`.
+ """
+
+ scheme = None
+ QueueCls = queue.LifoQueue
+
+ def __init__(self, host, port=None):
+ if not host:
+ raise LocationValueError("No host specified.")
+
+ self.host = _ipv6_host(host).lower()
+ self.port = port
+
+ def __str__(self):
+ return '%s(host=%r, port=%r)' % (type(self).__name__,
+ self.host, self.port)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.close()
+ # Return False to re-raise any potential exceptions
+ return False
+
+ def close(self):
+ """
+ Close all pooled connections and disable the pool.
+ """
+ pass
+
+
+# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252
+_blocking_errnos = set([errno.EAGAIN, errno.EWOULDBLOCK])
+
+
+class HTTPConnectionPool(ConnectionPool, RequestMethods):
+ """
+ Thread-safe connection pool for one host.
+
+ :param host:
+ Host used for this HTTP Connection (e.g. "localhost"), passed into
+ :class:`httplib.HTTPConnection`.
+
+ :param port:
+ Port used for this HTTP Connection (None is equivalent to 80), passed
+ into :class:`httplib.HTTPConnection`.
+
+ :param strict:
+ Causes BadStatusLine to be raised if the status line can't be parsed
+ as a valid HTTP/1.0 or 1.1 status line, passed into
+ :class:`httplib.HTTPConnection`.
+
+ .. note::
+ Only works in Python 2. This parameter is ignored in Python 3.
+
+ :param timeout:
+ Socket timeout in seconds for each individual connection. This can
+ be a float or integer, which sets the timeout for the HTTP request,
+ or an instance of :class:`urllib3.util.Timeout` which gives you more
+ fine-grained control over request timeouts. After the constructor has
+ been parsed, this is always a `urllib3.util.Timeout` object.
+
+ :param maxsize:
+ Number of connections to save that can be reused. More than 1 is useful
+ in multithreaded situations. If ``block`` is set to False, more
+ connections will be created but they will not be saved once they've
+ been used.
+
+ :param block:
+ If set to True, no more than ``maxsize`` connections will be used at
+ a time. When no free connections are available, the call will block
+ until a connection has been released. This is a useful side effect for
+ particular multithreaded situations where one does not want to use more
+ than maxsize connections per host to prevent flooding.
+
+ :param headers:
+ Headers to include with all requests, unless other headers are given
+ explicitly.
+
+ :param retries:
+ Retry configuration to use by default with requests in this pool.
+
+ :param _proxy:
+ Parsed proxy URL, should not be used directly, instead, see
+ :class:`urllib3.connectionpool.ProxyManager`"
+
+ :param _proxy_headers:
+ A dictionary with proxy headers, should not be used directly,
+ instead, see :class:`urllib3.connectionpool.ProxyManager`"
+
+ :param \\**conn_kw:
+ Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`,
+ :class:`urllib3.connection.HTTPSConnection` instances.
+ """
+
+ scheme = 'http'
+ ConnectionCls = HTTPConnection
+ ResponseCls = HTTPResponse
+
+ def __init__(self, host, port=None, strict=False,
+ timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False,
+ headers=None, retries=None,
+ _proxy=None, _proxy_headers=None,
+ **conn_kw):
+ ConnectionPool.__init__(self, host, port)
+ RequestMethods.__init__(self, headers)
+
+ self.strict = strict
+
+ if not isinstance(timeout, Timeout):
+ timeout = Timeout.from_float(timeout)
+
+ if retries is None:
+ retries = Retry.DEFAULT
+
+ self.timeout = timeout
+ self.retries = retries
+
+ self.pool = self.QueueCls(maxsize)
+ self.block = block
+
+ self.proxy = _proxy
+ self.proxy_headers = _proxy_headers or {}
+
+ # Fill the queue up so that doing get() on it will block properly
+ for _ in xrange(maxsize):
+ self.pool.put(None)
+
+ # These are mostly for testing and debugging purposes.
+ self.num_connections = 0
+ self.num_requests = 0
+ self.conn_kw = conn_kw
+
+ if self.proxy:
+ # Enable Nagle's algorithm for proxies, to avoid packet fragmentation.
+ # We cannot know if the user has added default socket options, so we cannot replace the
+ # list.
+ self.conn_kw.setdefault('socket_options', [])
+
+ def _new_conn(self):
+ """
+ Return a fresh :class:`HTTPConnection`.
+ """
+ self.num_connections += 1
+ log.debug("Starting new HTTP connection (%d): %s",
+ self.num_connections, self.host)
+
+ conn = self.ConnectionCls(host=self.host, port=self.port,
+ timeout=self.timeout.connect_timeout,
+ strict=self.strict, **self.conn_kw)
+ return conn
+
+ def _get_conn(self, timeout=None):
+ """
+ Get a connection. Will return a pooled connection if one is available.
+
+ If no connections are available and :prop:`.block` is ``False``, then a
+ fresh connection is returned.
+
+ :param timeout:
+ Seconds to wait before giving up and raising
+ :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and
+ :prop:`.block` is ``True``.
+ """
+ conn = None
+ try:
+ conn = self.pool.get(block=self.block, timeout=timeout)
+
+ except AttributeError: # self.pool is None
+ raise ClosedPoolError(self, "Pool is closed.")
+
+ except queue.Empty:
+ if self.block:
+ raise EmptyPoolError(self,
+ "Pool reached maximum size and no more "
+ "connections are allowed.")
+ pass # Oh well, we'll create a new connection then
+
+ # If this is a persistent connection, check if it got disconnected
+ if conn and is_connection_dropped(conn):
+ log.debug("Resetting dropped connection: %s", self.host)
+ conn.close()
+ if getattr(conn, 'auto_open', 1) == 0:
+ # This is a proxied connection that has been mutated by
+ # httplib._tunnel() and cannot be reused (since it would
+ # attempt to bypass the proxy)
+ conn = None
+
+ return conn or self._new_conn()
+
+ def _put_conn(self, conn):
+ """
+ Put a connection back into the pool.
+
+ :param conn:
+ Connection object for the current host and port as returned by
+ :meth:`._new_conn` or :meth:`._get_conn`.
+
+ If the pool is already full, the connection is closed and discarded
+ because we exceeded maxsize. If connections are discarded frequently,
+ then maxsize should be increased.
+
+ If the pool is closed, then the connection will be closed and discarded.
+ """
+ try:
+ self.pool.put(conn, block=False)
+ return # Everything is dandy, done.
+ except AttributeError:
+ # self.pool is None.
+ pass
+ except queue.Full:
+ # This should never happen if self.block == True
+ log.warning(
+ "Connection pool is full, discarding connection: %s",
+ self.host)
+
+ # Connection never got put back into the pool, close it.
+ if conn:
+ conn.close()
+
+ def _validate_conn(self, conn):
+ """
+ Called right before a request is made, after the socket is created.
+ """
+ # Force connect early to allow us to set read timeout in time
+ if not getattr(conn, 'sock', None): # AppEngine might not have `.sock`
+ conn.connect()
+
+ def _prepare_proxy(self, conn):
+ # Nothing to do for HTTP connections.
+ pass
+
+ def _get_timeout(self, timeout):
+ """ Helper that always returns a :class:`urllib3.util.Timeout` """
+ if timeout is _Default:
+ return self.timeout.clone()
+
+ if isinstance(timeout, Timeout):
+ return timeout.clone()
+ else:
+ # User passed us an int/float. This is for backwards compatibility,
+ # can be removed later
+ return Timeout.from_float(timeout)
+
+ def _raise_timeout(self, err, url, timeout_value, exc_cls):
+ """Is the error actually a timeout? Will raise a ReadTimeout or pass"""
+
+ # exc_cls is either ReadTimeoutError or ConnectTimeoutError
+ # Only ReadTimeoutError requires the url (preserving old behaviour)
+ args = [self]
+ if exc_cls is ReadTimeoutError:
+ args.append(url)
+ desc = 'Read'
+ else:
+ desc = 'Connect'
+
+ if isinstance(err, SocketTimeout):
+ args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value))
+ raise exc_cls(*args)
+
+ # See the above comment about EAGAIN in Python 3. In Python 2 we have
+ # to specifically catch it and throw the timeout error
+ elif hasattr(err, 'errno') and err.errno in _blocking_errnos:
+ args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value))
+ raise exc_cls(*args)
+
+ # Catch possible read timeouts thrown as SSL errors. If not the
+ # case, rethrow the original. We need to do this because of:
+ # http://bugs.python.org/issue10272
+ elif 'timed out' in str(err) or 'did not complete (read)' in str(err): # Python 2.6
+ args.append("%s timed out. (%s timeout=%s)" % (desc, desc.lower(), timeout_value))
+ raise exc_cls(*args)
+
+ def _make_request(self, conn, method, url, timeout=_Default, chunked=False,
+ **httplib_request_kw):
+ """
+ Perform a request on a given urllib connection object taken from our
+ pool.
+
+ :param conn:
+ a connection from one of our connection pools
+
+ :param timeout:
+ Socket timeout in seconds for the request. This can be a
+ float or integer, which will set the same timeout value for
+ the socket connect and the socket read, or an instance of
+ :class:`urllib3.util.Timeout`, which gives you more fine-grained
+ control over your timeouts.
+ """
+ self.num_requests += 1
+
+ timeout_obj = self._get_timeout(timeout)
+ timeout_obj.start_connect()
+ conn.timeout = timeout_obj.connect_timeout
+
+ # Trigger any extra validation we need to do.
+ try:
+ self._validate_conn(conn)
+ except (SocketTimeout, BaseSSLError) as e:
+ # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.
+ self._raise_timeout(err=e, url=url, timeout_value=conn.timeout,
+ exc_cls=ConnectTimeoutError)
+ raise
+
+ # Reset the timeout for the recv() on the socket
+ read_timeout = timeout_obj.read_timeout
+
+ # App Engine doesn't have a sock attr
+ if getattr(conn, 'sock', None):
+ # In Python 3 socket.py will catch EAGAIN and return None when you
+ # try and read into the file pointer created by http.client, which
+ # instead raises a BadStatusLine exception. Instead of catching
+ # the exception and assuming all BadStatusLine exceptions are read
+ # timeouts, check for a zero timeout before making the request.
+ if read_timeout == 0:
+ raise ReadTimeoutError(
+ self, url, "Read timed out. (read timeout=%s)" % read_timeout)
+ if read_timeout is Timeout.DEFAULT_TIMEOUT:
+ conn.sock.settimeout(socket.getdefaulttimeout())
+ else: # None or a value
+ conn.sock.settimeout(read_timeout)
+
+ # conn.request() calls httplib.*.request, not the method in
+ # urllib3.request. It also calls makefile (recv) on the socket.
+ if chunked:
+ conn.request_chunked(method, url, **httplib_request_kw)
+ else:
+ conn.request(method, url, **httplib_request_kw)
+
+ # Receive the response from the server
+ try:
+ try: # Python 2.7, use buffering of HTTP responses
+ httplib_response = conn.getresponse(buffering=True)
+ except TypeError: # Python 2.6 and older, Python 3
+ try:
+ httplib_response = conn.getresponse()
+ except Exception as e:
+ # Remove the TypeError from the exception chain in Python 3;
+ # otherwise it looks like a programming error was the cause.
+ six.raise_from(e, None)
+ except (SocketTimeout, BaseSSLError, SocketError) as e:
+ self._raise_timeout(err=e, url=url, timeout_value=read_timeout,
+ exc_cls=ReadTimeoutError)
+ raise
+
+ # AppEngine doesn't have a version attr.
+ http_version = getattr(conn, '_http_vsn_str', 'HTTP/?')
+ log.debug("%s://%s:%s \"%s %s %s\" %s %s", self.scheme, self.host, self.port,
+ method, url, http_version, httplib_response.status,
+ httplib_response.length)
+
+ try:
+ assert_header_parsing(httplib_response.msg)
+ except HeaderParsingError as hpe: # Platform-specific: Python 3
+ log.warning(
+ 'Failed to parse headers (url=%s): %s',
+ self._absolute_url(url), hpe, exc_info=True)
+
+ return httplib_response
+
+ def _absolute_url(self, path):
+ return Url(scheme=self.scheme, host=self.host, port=self.port, path=path).url
+
+ def close(self):
+ """
+ Close all pooled connections and disable the pool.
+ """
+ # Disable access to the pool
+ old_pool, self.pool = self.pool, None
+
+ try:
+ while True:
+ conn = old_pool.get(block=False)
+ if conn:
+ conn.close()
+
+ except queue.Empty:
+ pass # Done.
+
+ def is_same_host(self, url):
+ """
+ Check if the given ``url`` is a member of the same host as this
+ connection pool.
+ """
+ if url.startswith('/'):
+ return True
+
+ # TODO: Add optional support for socket.gethostbyname checking.
+ scheme, host, port = get_host(url)
+
+ host = _ipv6_host(host).lower()
+
+ # Use explicit default port for comparison when none is given
+ if self.port and not port:
+ port = port_by_scheme.get(scheme)
+ elif not self.port and port == port_by_scheme.get(scheme):
+ port = None
+
+ return (scheme, host, port) == (self.scheme, self.host, self.port)
+
+ def urlopen(self, method, url, body=None, headers=None, retries=None,
+ redirect=True, assert_same_host=True, timeout=_Default,
+ pool_timeout=None, release_conn=None, chunked=False,
+ body_pos=None, **response_kw):
+ """
+ Get a connection from the pool and perform an HTTP request. This is the
+ lowest level call for making a request, so you'll need to specify all
+ the raw details.
+
+ .. note::
+
+ More commonly, it's appropriate to use a convenience method provided
+ by :class:`.RequestMethods`, such as :meth:`request`.
+
+ .. note::
+
+ `release_conn` will only behave as expected if
+ `preload_content=False` because we want to make
+ `preload_content=False` the default behaviour someday soon without
+ breaking backwards compatibility.
+
+ :param method:
+ HTTP request method (such as GET, POST, PUT, etc.)
+
+ :param body:
+ Data to send in the request body (useful for creating
+ POST requests, see HTTPConnectionPool.post_url for
+ more convenience).
+
+ :param headers:
+ Dictionary of custom headers to send, such as User-Agent,
+ If-None-Match, etc. If None, pool headers are used. If provided,
+ these headers completely replace any pool-specific headers.
+
+ :param retries:
+ Configure the number of retries to allow before raising a
+ :class:`~urllib3.exceptions.MaxRetryError` exception.
+
+ Pass ``None`` to retry until you receive a response. Pass a
+ :class:`~urllib3.util.retry.Retry` object for fine-grained control
+ over different types of retries.
+ Pass an integer number to retry connection errors that many times,
+ but no other types of errors. Pass zero to never retry.
+
+ If ``False``, then retries are disabled and any exception is raised
+ immediately. Also, instead of raising a MaxRetryError on redirects,
+ the redirect response will be returned.
+
+ :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int.
+
+ :param redirect:
+ If True, automatically handle redirects (status codes 301, 302,
+ 303, 307, 308). Each redirect counts as a retry. Disabling retries
+ will disable redirect, too.
+
+ :param assert_same_host:
+ If ``True``, will make sure that the host of the pool requests is
+ consistent else will raise HostChangedError. When False, you can
+ use the pool on an HTTP proxy and request foreign hosts.
+
+ :param timeout:
+ If specified, overrides the default timeout for this one
+ request. It may be a float (in seconds) or an instance of
+ :class:`urllib3.util.Timeout`.
+
+ :param pool_timeout:
+ If set and the pool is set to block=True, then this method will
+ block for ``pool_timeout`` seconds and raise EmptyPoolError if no
+ connection is available within the time period.
+
+ :param release_conn:
+ If False, then the urlopen call will not release the connection
+ back into the pool once a response is received (but will release if
+ you read the entire contents of the response such as when
+ `preload_content=True`). This is useful if you're not preloading
+ the response's content immediately. You will need to call
+ ``r.release_conn()`` on the response ``r`` to return the connection
+ back into the pool. If None, it takes the value of
+ ``response_kw.get('preload_content', True)``.
+
+ :param chunked:
+ If True, urllib3 will send the body using chunked transfer
+ encoding. Otherwise, urllib3 will send the body using the standard
+ content-length form. Defaults to False.
+
+ :param int body_pos:
+ Position to seek to in file-like body in the event of a retry or
+ redirect. Typically this won't need to be set because urllib3 will
+ auto-populate the value when needed.
+
+ :param \\**response_kw:
+ Additional parameters are passed to
+ :meth:`urllib3.response.HTTPResponse.from_httplib`
+ """
+ if headers is None:
+ headers = self.headers
+
+ if not isinstance(retries, Retry):
+ retries = Retry.from_int(retries, redirect=redirect, default=self.retries)
+
+ if release_conn is None:
+ release_conn = response_kw.get('preload_content', True)
+
+ # Check host
+ if assert_same_host and not self.is_same_host(url):
+ raise HostChangedError(self, url, retries)
+
+ conn = None
+
+ # Track whether `conn` needs to be released before
+ # returning/raising/recursing. Update this variable if necessary, and
+ # leave `release_conn` constant throughout the function. That way, if
+ # the function recurses, the original value of `release_conn` will be
+ # passed down into the recursive call, and its value will be respected.
+ #
+ # See issue #651 [1] for details.
+ #
+ # [1]
+ release_this_conn = release_conn
+
+ # Merge the proxy headers. Only do this in HTTP. We have to copy the
+ # headers dict so we can safely change it without those changes being
+ # reflected in anyone else's copy.
+ if self.scheme == 'http':
+ headers = headers.copy()
+ headers.update(self.proxy_headers)
+
+ # Must keep the exception bound to a separate variable or else Python 3
+ # complains about UnboundLocalError.
+ err = None
+
+ # Keep track of whether we cleanly exited the except block. This
+ # ensures we do proper cleanup in finally.
+ clean_exit = False
+
+ # Rewind body position, if needed. Record current position
+ # for future rewinds in the event of a redirect/retry.
+ body_pos = set_file_position(body, body_pos)
+
+ try:
+ # Request a connection from the queue.
+ timeout_obj = self._get_timeout(timeout)
+ conn = self._get_conn(timeout=pool_timeout)
+
+ conn.timeout = timeout_obj.connect_timeout
+
+ is_new_proxy_conn = self.proxy is not None and not getattr(conn, 'sock', None)
+ if is_new_proxy_conn:
+ self._prepare_proxy(conn)
+
+ # Make the request on the httplib connection object.
+ httplib_response = self._make_request(conn, method, url,
+ timeout=timeout_obj,
+ body=body, headers=headers,
+ chunked=chunked)
+
+ # If we're going to release the connection in ``finally:``, then
+ # the response doesn't need to know about the connection. Otherwise
+ # it will also try to release it and we'll have a double-release
+ # mess.
+ response_conn = conn if not release_conn else None
+
+ # Pass method to Response for length checking
+ response_kw['request_method'] = method
+
+ # Import httplib's response into our own wrapper object
+ response = self.ResponseCls.from_httplib(httplib_response,
+ pool=self,
+ connection=response_conn,
+ retries=retries,
+ **response_kw)
+
+ # Everything went great!
+ clean_exit = True
+
+ except queue.Empty:
+ # Timed out by queue.
+ raise EmptyPoolError(self, "No pool connections are available.")
+
+ except (BaseSSLError, CertificateError) as e:
+ # Close the connection. If a connection is reused on which there
+ # was a Certificate error, the next request will certainly raise
+ # another Certificate error.
+ clean_exit = False
+ raise SSLError(e)
+
+ except SSLError:
+ # Treat SSLError separately from BaseSSLError to preserve
+ # traceback.
+ clean_exit = False
+ raise
+
+ except (TimeoutError, HTTPException, SocketError, ProtocolError) as e:
+ # Discard the connection for these exceptions. It will be
+ # be replaced during the next _get_conn() call.
+ clean_exit = False
+
+ if isinstance(e, (SocketError, NewConnectionError)) and self.proxy:
+ e = ProxyError('Cannot connect to proxy.', e)
+ elif isinstance(e, (SocketError, HTTPException)):
+ e = ProtocolError('Connection aborted.', e)
+
+ retries = retries.increment(method, url, error=e, _pool=self,
+ _stacktrace=sys.exc_info()[2])
+ retries.sleep()
+
+ # Keep track of the error for the retry warning.
+ err = e
+
+ finally:
+ if not clean_exit:
+ # We hit some kind of exception, handled or otherwise. We need
+ # to throw the connection away unless explicitly told not to.
+ # Close the connection, set the variable to None, and make sure
+ # we put the None back in the pool to avoid leaking it.
+ conn = conn and conn.close()
+ release_this_conn = True
+
+ if release_this_conn:
+ # Put the connection back to be reused. If the connection is
+ # expired then it will be None, which will get replaced with a
+ # fresh connection during _get_conn.
+ self._put_conn(conn)
+
+ if not conn:
+ # Try again
+ log.warning("Retrying (%r) after connection "
+ "broken by '%r': %s", retries, err, url)
+ return self.urlopen(method, url, body, headers, retries,
+ redirect, assert_same_host,
+ timeout=timeout, pool_timeout=pool_timeout,
+ release_conn=release_conn, body_pos=body_pos,
+ **response_kw)
+
+ # Handle redirect?
+ redirect_location = redirect and response.get_redirect_location()
+ if redirect_location:
+ if response.status == 303:
+ method = 'GET'
+
+ try:
+ retries = retries.increment(method, url, response=response, _pool=self)
+ except MaxRetryError:
+ if retries.raise_on_redirect:
+ # Release the connection for this response, since we're not
+ # returning it to be released manually.
+ response.release_conn()
+ raise
+ return response
+
+ retries.sleep_for_retry(response)
+ log.debug("Redirecting %s -> %s", url, redirect_location)
+ return self.urlopen(
+ method, redirect_location, body, headers,
+ retries=retries, redirect=redirect,
+ assert_same_host=assert_same_host,
+ timeout=timeout, pool_timeout=pool_timeout,
+ release_conn=release_conn, body_pos=body_pos,
+ **response_kw)
+
+ # Check if we should retry the HTTP response.
+ has_retry_after = bool(response.getheader('Retry-After'))
+ if retries.is_retry(method, response.status, has_retry_after):
+ try:
+ retries = retries.increment(method, url, response=response, _pool=self)
+ except MaxRetryError:
+ if retries.raise_on_status:
+ # Release the connection for this response, since we're not
+ # returning it to be released manually.
+ response.release_conn()
+ raise
+ return response
+ retries.sleep(response)
+ log.debug("Retry: %s", url)
+ return self.urlopen(
+ method, url, body, headers,
+ retries=retries, redirect=redirect,
+ assert_same_host=assert_same_host,
+ timeout=timeout, pool_timeout=pool_timeout,
+ release_conn=release_conn,
+ body_pos=body_pos, **response_kw)
+
+ return response
+
+
+class HTTPSConnectionPool(HTTPConnectionPool):
+ """
+ Same as :class:`.HTTPConnectionPool`, but HTTPS.
+
+ When Python is compiled with the :mod:`ssl` module, then
+ :class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates,
+ instead of :class:`.HTTPSConnection`.
+
+ :class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``,
+ ``assert_hostname`` and ``host`` in this order to verify connections.
+ If ``assert_hostname`` is False, no verification is done.
+
+ The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``,
+ ``ca_cert_dir``, and ``ssl_version`` are only used if :mod:`ssl` is
+ available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade
+ the connection socket into an SSL socket.
+ """
+
+ scheme = 'https'
+ ConnectionCls = HTTPSConnection
+
+ def __init__(self, host, port=None,
+ strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1,
+ block=False, headers=None, retries=None,
+ _proxy=None, _proxy_headers=None,
+ key_file=None, cert_file=None, cert_reqs=None,
+ ca_certs=None, ssl_version=None,
+ assert_hostname=None, assert_fingerprint=None,
+ ca_cert_dir=None, **conn_kw):
+
+ HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize,
+ block, headers, retries, _proxy, _proxy_headers,
+ **conn_kw)
+
+ if ca_certs and cert_reqs is None:
+ cert_reqs = 'CERT_REQUIRED'
+
+ self.key_file = key_file
+ self.cert_file = cert_file
+ self.cert_reqs = cert_reqs
+ self.ca_certs = ca_certs
+ self.ca_cert_dir = ca_cert_dir
+ self.ssl_version = ssl_version
+ self.assert_hostname = assert_hostname
+ self.assert_fingerprint = assert_fingerprint
+
+ def _prepare_conn(self, conn):
+ """
+ Prepare the ``connection`` for :meth:`urllib3.util.ssl_wrap_socket`
+ and establish the tunnel if proxy is used.
+ """
+
+ if isinstance(conn, VerifiedHTTPSConnection):
+ conn.set_cert(key_file=self.key_file,
+ cert_file=self.cert_file,
+ cert_reqs=self.cert_reqs,
+ ca_certs=self.ca_certs,
+ ca_cert_dir=self.ca_cert_dir,
+ assert_hostname=self.assert_hostname,
+ assert_fingerprint=self.assert_fingerprint)
+ conn.ssl_version = self.ssl_version
+ return conn
+
+ def _prepare_proxy(self, conn):
+ """
+ Establish tunnel connection early, because otherwise httplib
+ would improperly set Host: header to proxy's IP:port.
+ """
+ # Python 2.7+
+ try:
+ set_tunnel = conn.set_tunnel
+ except AttributeError: # Platform-specific: Python 2.6
+ set_tunnel = conn._set_tunnel
+
+ if sys.version_info <= (2, 6, 4) and not self.proxy_headers: # Python 2.6.4 and older
+ set_tunnel(self.host, self.port)
+ else:
+ set_tunnel(self.host, self.port, self.proxy_headers)
+
+ conn.connect()
+
+ def _new_conn(self):
+ """
+ Return a fresh :class:`httplib.HTTPSConnection`.
+ """
+ self.num_connections += 1
+ log.debug("Starting new HTTPS connection (%d): %s",
+ self.num_connections, self.host)
+
+ if not self.ConnectionCls or self.ConnectionCls is DummyConnection:
+ raise SSLError("Can't connect to HTTPS URL because the SSL "
+ "module is not available.")
+
+ actual_host = self.host
+ actual_port = self.port
+ if self.proxy is not None:
+ actual_host = self.proxy.host
+ actual_port = self.proxy.port
+
+ conn = self.ConnectionCls(host=actual_host, port=actual_port,
+ timeout=self.timeout.connect_timeout,
+ strict=self.strict, **self.conn_kw)
+
+ return self._prepare_conn(conn)
+
+ def _validate_conn(self, conn):
+ """
+ Called right before a request is made, after the socket is created.
+ """
+ super(HTTPSConnectionPool, self)._validate_conn(conn)
+
+ if not conn.is_verified:
+ warnings.warn((
+ 'Unverified HTTPS request is being made. '
+ 'Adding certificate verification is strongly advised. See: '
+ 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html'
+ '#ssl-warnings'),
+ InsecureRequestWarning)
+
+
+def connection_from_url(url, **kw):
+ """
+ Given a url, return an :class:`.ConnectionPool` instance of its host.
+
+ This is a shortcut for not having to parse out the scheme, host, and port
+ of the url before creating an :class:`.ConnectionPool` instance.
+
+ :param url:
+ Absolute URL string that must include the scheme. Port is optional.
+
+ :param \\**kw:
+ Passes additional parameters to the constructor of the appropriate
+ :class:`.ConnectionPool`. Useful for specifying things like
+ timeout, maxsize, headers, etc.
+
+ Example::
+
+ >>> conn = connection_from_url('http://google.com/')
+ >>> r = conn.request('GET', '/')
+ """
+ scheme, host, port = get_host(url)
+ port = port or port_by_scheme.get(scheme, 80)
+ if scheme == 'https':
+ return HTTPSConnectionPool(host, port=port, **kw)
+ else:
+ return HTTPConnectionPool(host, port=port, **kw)
+
+
+def _ipv6_host(host):
+ """
+ Process IPv6 address literals
+ """
+
+ # httplib doesn't like it when we include brackets in IPv6 addresses
+ # Specifically, if we include brackets but also pass the port then
+ # httplib crazily doubles up the square brackets on the Host header.
+ # Instead, we need to make sure we never pass ``None`` as the port.
+ # However, for backward compatibility reasons we can't actually
+ # *assert* that. See http://bugs.python.org/issue28539
+ #
+ # Also if an IPv6 address literal has a zone identifier, the
+ # percent sign might be URIencoded, convert it back into ASCII
+ if host.startswith('[') and host.endswith(']'):
+ host = host.replace('%25', '%').strip('[]')
+ return host
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/appengine.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/appengine.py
index 6eb75a0..814b022 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/appengine.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/appengine.py
@@ -1,296 +1,296 @@
-"""
-This module provides a pool manager that uses Google App Engine's
-`URLFetch Service `_.
-
-Example usage::
-
- from urllib3 import PoolManager
- from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox
-
- if is_appengine_sandbox():
- # AppEngineManager uses AppEngine's URLFetch API behind the scenes
- http = AppEngineManager()
- else:
- # PoolManager uses a socket-level API behind the scenes
- http = PoolManager()
-
- r = http.request('GET', 'https://google.com/')
-
-There are `limitations `_ to the URLFetch service and it may not be
-the best choice for your application. There are three options for using
-urllib3 on Google App Engine:
-
-1. You can use :class:`AppEngineManager` with URLFetch. URLFetch is
- cost-effective in many circumstances as long as your usage is within the
- limitations.
-2. You can use a normal :class:`~urllib3.PoolManager` by enabling sockets.
- Sockets also have `limitations and restrictions
- `_ and have a lower free quota than URLFetch.
- To use sockets, be sure to specify the following in your ``app.yaml``::
-
- env_variables:
- GAE_USE_SOCKETS_HTTPLIB : 'true'
-
-3. If you are using `App Engine Flexible
-`_, you can use the standard
-:class:`PoolManager` without any configuration or special environment variables.
-"""
-
-from __future__ import absolute_import
-import logging
-import os
-import warnings
-from ..packages.six.moves.urllib.parse import urljoin
-
-from ..exceptions import (
- HTTPError,
- HTTPWarning,
- MaxRetryError,
- ProtocolError,
- TimeoutError,
- SSLError
-)
-
-from ..packages.six import BytesIO
-from ..request import RequestMethods
-from ..response import HTTPResponse
-from ..util.timeout import Timeout
-from ..util.retry import Retry
-
-try:
- from google.appengine.api import urlfetch
-except ImportError:
- urlfetch = None
-
-
-log = logging.getLogger(__name__)
-
-
-class AppEnginePlatformWarning(HTTPWarning):
- pass
-
-
-class AppEnginePlatformError(HTTPError):
- pass
-
-
-class AppEngineManager(RequestMethods):
- """
- Connection manager for Google App Engine sandbox applications.
-
- This manager uses the URLFetch service directly instead of using the
- emulated httplib, and is subject to URLFetch limitations as described in
- the App Engine documentation `here
- `_.
-
- Notably it will raise an :class:`AppEnginePlatformError` if:
- * URLFetch is not available.
- * If you attempt to use this on App Engine Flexible, as full socket
- support is available.
- * If a request size is more than 10 megabytes.
- * If a response size is more than 32 megabtyes.
- * If you use an unsupported request method such as OPTIONS.
-
- Beyond those cases, it will raise normal urllib3 errors.
- """
-
- def __init__(self, headers=None, retries=None, validate_certificate=True,
- urlfetch_retries=True):
- if not urlfetch:
- raise AppEnginePlatformError(
- "URLFetch is not available in this environment.")
-
- if is_prod_appengine_mvms():
- raise AppEnginePlatformError(
- "Use normal urllib3.PoolManager instead of AppEngineManager"
- "on Managed VMs, as using URLFetch is not necessary in "
- "this environment.")
-
- warnings.warn(
- "urllib3 is using URLFetch on Google App Engine sandbox instead "
- "of sockets. To use sockets directly instead of URLFetch see "
- "https://urllib3.readthedocs.io/en/latest/reference/urllib3.contrib.html.",
- AppEnginePlatformWarning)
-
- RequestMethods.__init__(self, headers)
- self.validate_certificate = validate_certificate
- self.urlfetch_retries = urlfetch_retries
-
- self.retries = retries or Retry.DEFAULT
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- # Return False to re-raise any potential exceptions
- return False
-
- def urlopen(self, method, url, body=None, headers=None,
- retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT,
- **response_kw):
-
- retries = self._get_retries(retries, redirect)
-
- try:
- follow_redirects = (
- redirect and
- retries.redirect != 0 and
- retries.total)
- response = urlfetch.fetch(
- url,
- payload=body,
- method=method,
- headers=headers or {},
- allow_truncated=False,
- follow_redirects=self.urlfetch_retries and follow_redirects,
- deadline=self._get_absolute_timeout(timeout),
- validate_certificate=self.validate_certificate,
- )
- except urlfetch.DeadlineExceededError as e:
- raise TimeoutError(self, e)
-
- except urlfetch.InvalidURLError as e:
- if 'too large' in str(e):
- raise AppEnginePlatformError(
- "URLFetch request too large, URLFetch only "
- "supports requests up to 10mb in size.", e)
- raise ProtocolError(e)
-
- except urlfetch.DownloadError as e:
- if 'Too many redirects' in str(e):
- raise MaxRetryError(self, url, reason=e)
- raise ProtocolError(e)
-
- except urlfetch.ResponseTooLargeError as e:
- raise AppEnginePlatformError(
- "URLFetch response too large, URLFetch only supports"
- "responses up to 32mb in size.", e)
-
- except urlfetch.SSLCertificateError as e:
- raise SSLError(e)
-
- except urlfetch.InvalidMethodError as e:
- raise AppEnginePlatformError(
- "URLFetch does not support method: %s" % method, e)
-
- http_response = self._urlfetch_response_to_http_response(
- response, retries=retries, **response_kw)
-
- # Handle redirect?
- redirect_location = redirect and http_response.get_redirect_location()
- if redirect_location:
- # Check for redirect response
- if (self.urlfetch_retries and retries.raise_on_redirect):
- raise MaxRetryError(self, url, "too many redirects")
- else:
- if http_response.status == 303:
- method = 'GET'
-
- try:
- retries = retries.increment(method, url, response=http_response, _pool=self)
- except MaxRetryError:
- if retries.raise_on_redirect:
- raise MaxRetryError(self, url, "too many redirects")
- return http_response
-
- retries.sleep_for_retry(http_response)
- log.debug("Redirecting %s -> %s", url, redirect_location)
- redirect_url = urljoin(url, redirect_location)
- return self.urlopen(
- method, redirect_url, body, headers,
- retries=retries, redirect=redirect,
- timeout=timeout, **response_kw)
-
- # Check if we should retry the HTTP response.
- has_retry_after = bool(http_response.getheader('Retry-After'))
- if retries.is_retry(method, http_response.status, has_retry_after):
- retries = retries.increment(
- method, url, response=http_response, _pool=self)
- log.debug("Retry: %s", url)
- retries.sleep(http_response)
- return self.urlopen(
- method, url,
- body=body, headers=headers,
- retries=retries, redirect=redirect,
- timeout=timeout, **response_kw)
-
- return http_response
-
- def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw):
-
- if is_prod_appengine():
- # Production GAE handles deflate encoding automatically, but does
- # not remove the encoding header.
- content_encoding = urlfetch_resp.headers.get('content-encoding')
-
- if content_encoding == 'deflate':
- del urlfetch_resp.headers['content-encoding']
-
- transfer_encoding = urlfetch_resp.headers.get('transfer-encoding')
- # We have a full response's content,
- # so let's make sure we don't report ourselves as chunked data.
- if transfer_encoding == 'chunked':
- encodings = transfer_encoding.split(",")
- encodings.remove('chunked')
- urlfetch_resp.headers['transfer-encoding'] = ','.join(encodings)
-
- return HTTPResponse(
- # In order for decoding to work, we must present the content as
- # a file-like object.
- body=BytesIO(urlfetch_resp.content),
- headers=urlfetch_resp.headers,
- status=urlfetch_resp.status_code,
- **response_kw
- )
-
- def _get_absolute_timeout(self, timeout):
- if timeout is Timeout.DEFAULT_TIMEOUT:
- return None # Defer to URLFetch's default.
- if isinstance(timeout, Timeout):
- if timeout._read is not None or timeout._connect is not None:
- warnings.warn(
- "URLFetch does not support granular timeout settings, "
- "reverting to total or default URLFetch timeout.",
- AppEnginePlatformWarning)
- return timeout.total
- return timeout
-
- def _get_retries(self, retries, redirect):
- if not isinstance(retries, Retry):
- retries = Retry.from_int(
- retries, redirect=redirect, default=self.retries)
-
- if retries.connect or retries.read or retries.redirect:
- warnings.warn(
- "URLFetch only supports total retries and does not "
- "recognize connect, read, or redirect retry parameters.",
- AppEnginePlatformWarning)
-
- return retries
-
-
-def is_appengine():
- return (is_local_appengine() or
- is_prod_appengine() or
- is_prod_appengine_mvms())
-
-
-def is_appengine_sandbox():
- return is_appengine() and not is_prod_appengine_mvms()
-
-
-def is_local_appengine():
- return ('APPENGINE_RUNTIME' in os.environ and
- 'Development/' in os.environ['SERVER_SOFTWARE'])
-
-
-def is_prod_appengine():
- return ('APPENGINE_RUNTIME' in os.environ and
- 'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and
- not is_prod_appengine_mvms())
-
-
-def is_prod_appengine_mvms():
- return os.environ.get('GAE_VM', False) == 'true'
+"""
+This module provides a pool manager that uses Google App Engine's
+`URLFetch Service `_.
+
+Example usage::
+
+ from urllib3 import PoolManager
+ from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox
+
+ if is_appengine_sandbox():
+ # AppEngineManager uses AppEngine's URLFetch API behind the scenes
+ http = AppEngineManager()
+ else:
+ # PoolManager uses a socket-level API behind the scenes
+ http = PoolManager()
+
+ r = http.request('GET', 'https://google.com/')
+
+There are `limitations `_ to the URLFetch service and it may not be
+the best choice for your application. There are three options for using
+urllib3 on Google App Engine:
+
+1. You can use :class:`AppEngineManager` with URLFetch. URLFetch is
+ cost-effective in many circumstances as long as your usage is within the
+ limitations.
+2. You can use a normal :class:`~urllib3.PoolManager` by enabling sockets.
+ Sockets also have `limitations and restrictions
+ `_ and have a lower free quota than URLFetch.
+ To use sockets, be sure to specify the following in your ``app.yaml``::
+
+ env_variables:
+ GAE_USE_SOCKETS_HTTPLIB : 'true'
+
+3. If you are using `App Engine Flexible
+`_, you can use the standard
+:class:`PoolManager` without any configuration or special environment variables.
+"""
+
+from __future__ import absolute_import
+import logging
+import os
+import warnings
+from ..packages.six.moves.urllib.parse import urljoin
+
+from ..exceptions import (
+ HTTPError,
+ HTTPWarning,
+ MaxRetryError,
+ ProtocolError,
+ TimeoutError,
+ SSLError
+)
+
+from ..packages.six import BytesIO
+from ..request import RequestMethods
+from ..response import HTTPResponse
+from ..util.timeout import Timeout
+from ..util.retry import Retry
+
+try:
+ from google.appengine.api import urlfetch
+except ImportError:
+ urlfetch = None
+
+
+log = logging.getLogger(__name__)
+
+
+class AppEnginePlatformWarning(HTTPWarning):
+ pass
+
+
+class AppEnginePlatformError(HTTPError):
+ pass
+
+
+class AppEngineManager(RequestMethods):
+ """
+ Connection manager for Google App Engine sandbox applications.
+
+ This manager uses the URLFetch service directly instead of using the
+ emulated httplib, and is subject to URLFetch limitations as described in
+ the App Engine documentation `here
+ `_.
+
+ Notably it will raise an :class:`AppEnginePlatformError` if:
+ * URLFetch is not available.
+ * If you attempt to use this on App Engine Flexible, as full socket
+ support is available.
+ * If a request size is more than 10 megabytes.
+ * If a response size is more than 32 megabtyes.
+ * If you use an unsupported request method such as OPTIONS.
+
+ Beyond those cases, it will raise normal urllib3 errors.
+ """
+
+ def __init__(self, headers=None, retries=None, validate_certificate=True,
+ urlfetch_retries=True):
+ if not urlfetch:
+ raise AppEnginePlatformError(
+ "URLFetch is not available in this environment.")
+
+ if is_prod_appengine_mvms():
+ raise AppEnginePlatformError(
+ "Use normal urllib3.PoolManager instead of AppEngineManager"
+ "on Managed VMs, as using URLFetch is not necessary in "
+ "this environment.")
+
+ warnings.warn(
+ "urllib3 is using URLFetch on Google App Engine sandbox instead "
+ "of sockets. To use sockets directly instead of URLFetch see "
+ "https://urllib3.readthedocs.io/en/latest/reference/urllib3.contrib.html.",
+ AppEnginePlatformWarning)
+
+ RequestMethods.__init__(self, headers)
+ self.validate_certificate = validate_certificate
+ self.urlfetch_retries = urlfetch_retries
+
+ self.retries = retries or Retry.DEFAULT
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ # Return False to re-raise any potential exceptions
+ return False
+
+ def urlopen(self, method, url, body=None, headers=None,
+ retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT,
+ **response_kw):
+
+ retries = self._get_retries(retries, redirect)
+
+ try:
+ follow_redirects = (
+ redirect and
+ retries.redirect != 0 and
+ retries.total)
+ response = urlfetch.fetch(
+ url,
+ payload=body,
+ method=method,
+ headers=headers or {},
+ allow_truncated=False,
+ follow_redirects=self.urlfetch_retries and follow_redirects,
+ deadline=self._get_absolute_timeout(timeout),
+ validate_certificate=self.validate_certificate,
+ )
+ except urlfetch.DeadlineExceededError as e:
+ raise TimeoutError(self, e)
+
+ except urlfetch.InvalidURLError as e:
+ if 'too large' in str(e):
+ raise AppEnginePlatformError(
+ "URLFetch request too large, URLFetch only "
+ "supports requests up to 10mb in size.", e)
+ raise ProtocolError(e)
+
+ except urlfetch.DownloadError as e:
+ if 'Too many redirects' in str(e):
+ raise MaxRetryError(self, url, reason=e)
+ raise ProtocolError(e)
+
+ except urlfetch.ResponseTooLargeError as e:
+ raise AppEnginePlatformError(
+ "URLFetch response too large, URLFetch only supports"
+ "responses up to 32mb in size.", e)
+
+ except urlfetch.SSLCertificateError as e:
+ raise SSLError(e)
+
+ except urlfetch.InvalidMethodError as e:
+ raise AppEnginePlatformError(
+ "URLFetch does not support method: %s" % method, e)
+
+ http_response = self._urlfetch_response_to_http_response(
+ response, retries=retries, **response_kw)
+
+ # Handle redirect?
+ redirect_location = redirect and http_response.get_redirect_location()
+ if redirect_location:
+ # Check for redirect response
+ if (self.urlfetch_retries and retries.raise_on_redirect):
+ raise MaxRetryError(self, url, "too many redirects")
+ else:
+ if http_response.status == 303:
+ method = 'GET'
+
+ try:
+ retries = retries.increment(method, url, response=http_response, _pool=self)
+ except MaxRetryError:
+ if retries.raise_on_redirect:
+ raise MaxRetryError(self, url, "too many redirects")
+ return http_response
+
+ retries.sleep_for_retry(http_response)
+ log.debug("Redirecting %s -> %s", url, redirect_location)
+ redirect_url = urljoin(url, redirect_location)
+ return self.urlopen(
+ method, redirect_url, body, headers,
+ retries=retries, redirect=redirect,
+ timeout=timeout, **response_kw)
+
+ # Check if we should retry the HTTP response.
+ has_retry_after = bool(http_response.getheader('Retry-After'))
+ if retries.is_retry(method, http_response.status, has_retry_after):
+ retries = retries.increment(
+ method, url, response=http_response, _pool=self)
+ log.debug("Retry: %s", url)
+ retries.sleep(http_response)
+ return self.urlopen(
+ method, url,
+ body=body, headers=headers,
+ retries=retries, redirect=redirect,
+ timeout=timeout, **response_kw)
+
+ return http_response
+
+ def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw):
+
+ if is_prod_appengine():
+ # Production GAE handles deflate encoding automatically, but does
+ # not remove the encoding header.
+ content_encoding = urlfetch_resp.headers.get('content-encoding')
+
+ if content_encoding == 'deflate':
+ del urlfetch_resp.headers['content-encoding']
+
+ transfer_encoding = urlfetch_resp.headers.get('transfer-encoding')
+ # We have a full response's content,
+ # so let's make sure we don't report ourselves as chunked data.
+ if transfer_encoding == 'chunked':
+ encodings = transfer_encoding.split(",")
+ encodings.remove('chunked')
+ urlfetch_resp.headers['transfer-encoding'] = ','.join(encodings)
+
+ return HTTPResponse(
+ # In order for decoding to work, we must present the content as
+ # a file-like object.
+ body=BytesIO(urlfetch_resp.content),
+ headers=urlfetch_resp.headers,
+ status=urlfetch_resp.status_code,
+ **response_kw
+ )
+
+ def _get_absolute_timeout(self, timeout):
+ if timeout is Timeout.DEFAULT_TIMEOUT:
+ return None # Defer to URLFetch's default.
+ if isinstance(timeout, Timeout):
+ if timeout._read is not None or timeout._connect is not None:
+ warnings.warn(
+ "URLFetch does not support granular timeout settings, "
+ "reverting to total or default URLFetch timeout.",
+ AppEnginePlatformWarning)
+ return timeout.total
+ return timeout
+
+ def _get_retries(self, retries, redirect):
+ if not isinstance(retries, Retry):
+ retries = Retry.from_int(
+ retries, redirect=redirect, default=self.retries)
+
+ if retries.connect or retries.read or retries.redirect:
+ warnings.warn(
+ "URLFetch only supports total retries and does not "
+ "recognize connect, read, or redirect retry parameters.",
+ AppEnginePlatformWarning)
+
+ return retries
+
+
+def is_appengine():
+ return (is_local_appengine() or
+ is_prod_appengine() or
+ is_prod_appengine_mvms())
+
+
+def is_appengine_sandbox():
+ return is_appengine() and not is_prod_appengine_mvms()
+
+
+def is_local_appengine():
+ return ('APPENGINE_RUNTIME' in os.environ and
+ 'Development/' in os.environ['SERVER_SOFTWARE'])
+
+
+def is_prod_appengine():
+ return ('APPENGINE_RUNTIME' in os.environ and
+ 'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and
+ not is_prod_appengine_mvms())
+
+
+def is_prod_appengine_mvms():
+ return os.environ.get('GAE_VM', False) == 'true'
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/ntlmpool.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/ntlmpool.py
index 888e0ad..642e99e 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/ntlmpool.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/ntlmpool.py
@@ -1,112 +1,112 @@
-"""
-NTLM authenticating pool, contributed by erikcederstran
-
-Issue #10, see: http://code.google.com/p/urllib3/issues/detail?id=10
-"""
-from __future__ import absolute_import
-
-from logging import getLogger
-from ntlm import ntlm
-
-from .. import HTTPSConnectionPool
-from ..packages.six.moves.http_client import HTTPSConnection
-
-
-log = getLogger(__name__)
-
-
-class NTLMConnectionPool(HTTPSConnectionPool):
- """
- Implements an NTLM authentication version of an urllib3 connection pool
- """
-
- scheme = 'https'
-
- def __init__(self, user, pw, authurl, *args, **kwargs):
- """
- authurl is a random URL on the server that is protected by NTLM.
- user is the Windows user, probably in the DOMAIN\\username format.
- pw is the password for the user.
- """
- super(NTLMConnectionPool, self).__init__(*args, **kwargs)
- self.authurl = authurl
- self.rawuser = user
- user_parts = user.split('\\', 1)
- self.domain = user_parts[0].upper()
- self.user = user_parts[1]
- self.pw = pw
-
- def _new_conn(self):
- # Performs the NTLM handshake that secures the connection. The socket
- # must be kept open while requests are performed.
- self.num_connections += 1
- log.debug('Starting NTLM HTTPS connection no. %d: https://%s%s',
- self.num_connections, self.host, self.authurl)
-
- headers = {}
- headers['Connection'] = 'Keep-Alive'
- req_header = 'Authorization'
- resp_header = 'www-authenticate'
-
- conn = HTTPSConnection(host=self.host, port=self.port)
-
- # Send negotiation message
- headers[req_header] = (
- 'NTLM %s' % ntlm.create_NTLM_NEGOTIATE_MESSAGE(self.rawuser))
- log.debug('Request headers: %s', headers)
- conn.request('GET', self.authurl, None, headers)
- res = conn.getresponse()
- reshdr = dict(res.getheaders())
- log.debug('Response status: %s %s', res.status, res.reason)
- log.debug('Response headers: %s', reshdr)
- log.debug('Response data: %s [...]', res.read(100))
-
- # Remove the reference to the socket, so that it can not be closed by
- # the response object (we want to keep the socket open)
- res.fp = None
-
- # Server should respond with a challenge message
- auth_header_values = reshdr[resp_header].split(', ')
- auth_header_value = None
- for s in auth_header_values:
- if s[:5] == 'NTLM ':
- auth_header_value = s[5:]
- if auth_header_value is None:
- raise Exception('Unexpected %s response header: %s' %
- (resp_header, reshdr[resp_header]))
-
- # Send authentication message
- ServerChallenge, NegotiateFlags = \
- ntlm.parse_NTLM_CHALLENGE_MESSAGE(auth_header_value)
- auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge,
- self.user,
- self.domain,
- self.pw,
- NegotiateFlags)
- headers[req_header] = 'NTLM %s' % auth_msg
- log.debug('Request headers: %s', headers)
- conn.request('GET', self.authurl, None, headers)
- res = conn.getresponse()
- log.debug('Response status: %s %s', res.status, res.reason)
- log.debug('Response headers: %s', dict(res.getheaders()))
- log.debug('Response data: %s [...]', res.read()[:100])
- if res.status != 200:
- if res.status == 401:
- raise Exception('Server rejected request: wrong '
- 'username or password')
- raise Exception('Wrong server response: %s %s' %
- (res.status, res.reason))
-
- res.fp = None
- log.debug('Connection established')
- return conn
-
- def urlopen(self, method, url, body=None, headers=None, retries=3,
- redirect=True, assert_same_host=True):
- if headers is None:
- headers = {}
- headers['Connection'] = 'Keep-Alive'
- return super(NTLMConnectionPool, self).urlopen(method, url, body,
- headers, retries,
- redirect,
- assert_same_host)
+"""
+NTLM authenticating pool, contributed by erikcederstran
+
+Issue #10, see: http://code.google.com/p/urllib3/issues/detail?id=10
+"""
+from __future__ import absolute_import
+
+from logging import getLogger
+from ntlm import ntlm
+
+from .. import HTTPSConnectionPool
+from ..packages.six.moves.http_client import HTTPSConnection
+
+
+log = getLogger(__name__)
+
+
+class NTLMConnectionPool(HTTPSConnectionPool):
+ """
+ Implements an NTLM authentication version of an urllib3 connection pool
+ """
+
+ scheme = 'https'
+
+ def __init__(self, user, pw, authurl, *args, **kwargs):
+ """
+ authurl is a random URL on the server that is protected by NTLM.
+ user is the Windows user, probably in the DOMAIN\\username format.
+ pw is the password for the user.
+ """
+ super(NTLMConnectionPool, self).__init__(*args, **kwargs)
+ self.authurl = authurl
+ self.rawuser = user
+ user_parts = user.split('\\', 1)
+ self.domain = user_parts[0].upper()
+ self.user = user_parts[1]
+ self.pw = pw
+
+ def _new_conn(self):
+ # Performs the NTLM handshake that secures the connection. The socket
+ # must be kept open while requests are performed.
+ self.num_connections += 1
+ log.debug('Starting NTLM HTTPS connection no. %d: https://%s%s',
+ self.num_connections, self.host, self.authurl)
+
+ headers = {}
+ headers['Connection'] = 'Keep-Alive'
+ req_header = 'Authorization'
+ resp_header = 'www-authenticate'
+
+ conn = HTTPSConnection(host=self.host, port=self.port)
+
+ # Send negotiation message
+ headers[req_header] = (
+ 'NTLM %s' % ntlm.create_NTLM_NEGOTIATE_MESSAGE(self.rawuser))
+ log.debug('Request headers: %s', headers)
+ conn.request('GET', self.authurl, None, headers)
+ res = conn.getresponse()
+ reshdr = dict(res.getheaders())
+ log.debug('Response status: %s %s', res.status, res.reason)
+ log.debug('Response headers: %s', reshdr)
+ log.debug('Response data: %s [...]', res.read(100))
+
+ # Remove the reference to the socket, so that it can not be closed by
+ # the response object (we want to keep the socket open)
+ res.fp = None
+
+ # Server should respond with a challenge message
+ auth_header_values = reshdr[resp_header].split(', ')
+ auth_header_value = None
+ for s in auth_header_values:
+ if s[:5] == 'NTLM ':
+ auth_header_value = s[5:]
+ if auth_header_value is None:
+ raise Exception('Unexpected %s response header: %s' %
+ (resp_header, reshdr[resp_header]))
+
+ # Send authentication message
+ ServerChallenge, NegotiateFlags = \
+ ntlm.parse_NTLM_CHALLENGE_MESSAGE(auth_header_value)
+ auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge,
+ self.user,
+ self.domain,
+ self.pw,
+ NegotiateFlags)
+ headers[req_header] = 'NTLM %s' % auth_msg
+ log.debug('Request headers: %s', headers)
+ conn.request('GET', self.authurl, None, headers)
+ res = conn.getresponse()
+ log.debug('Response status: %s %s', res.status, res.reason)
+ log.debug('Response headers: %s', dict(res.getheaders()))
+ log.debug('Response data: %s [...]', res.read()[:100])
+ if res.status != 200:
+ if res.status == 401:
+ raise Exception('Server rejected request: wrong '
+ 'username or password')
+ raise Exception('Wrong server response: %s %s' %
+ (res.status, res.reason))
+
+ res.fp = None
+ log.debug('Connection established')
+ return conn
+
+ def urlopen(self, method, url, body=None, headers=None, retries=3,
+ redirect=True, assert_same_host=True):
+ if headers is None:
+ headers = {}
+ headers['Connection'] = 'Keep-Alive'
+ return super(NTLMConnectionPool, self).urlopen(method, url, body,
+ headers, retries,
+ redirect,
+ assert_same_host)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/pyopenssl.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/pyopenssl.py
index 9c789f0..eb4d476 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/pyopenssl.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/pyopenssl.py
@@ -1,450 +1,450 @@
-"""
-SSL with SNI_-support for Python 2. Follow these instructions if you would
-like to verify SSL certificates in Python 2. Note, the default libraries do
-*not* do certificate checking; you need to do additional work to validate
-certificates yourself.
-
-This needs the following packages installed:
-
-* pyOpenSSL (tested with 16.0.0)
-* cryptography (minimum 1.3.4, from pyopenssl)
-* idna (minimum 2.0, from cryptography)
-
-However, pyopenssl depends on cryptography, which depends on idna, so while we
-use all three directly here we end up having relatively few packages required.
-
-You can install them with the following command:
-
- pip install pyopenssl cryptography idna
-
-To activate certificate checking, call
-:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code
-before you begin making HTTP requests. This can be done in a ``sitecustomize``
-module, or at any other time before your application begins using ``urllib3``,
-like this::
-
- try:
- import urllib3.contrib.pyopenssl
- urllib3.contrib.pyopenssl.inject_into_urllib3()
- except ImportError:
- pass
-
-Now you can use :mod:`urllib3` as you normally would, and it will support SNI
-when the required modules are installed.
-
-Activating this module also has the positive side effect of disabling SSL/TLS
-compression in Python 2 (see `CRIME attack`_).
-
-If you want to configure the default list of supported cipher suites, you can
-set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable.
-
-.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication
-.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit)
-"""
-from __future__ import absolute_import
-
-import OpenSSL.SSL
-from cryptography import x509
-from cryptography.hazmat.backends.openssl import backend as openssl_backend
-from cryptography.hazmat.backends.openssl.x509 import _Certificate
-
-from socket import timeout, error as SocketError
-from io import BytesIO
-
-try: # Platform-specific: Python 2
- from socket import _fileobject
-except ImportError: # Platform-specific: Python 3
- _fileobject = None
- from ..packages.backports.makefile import backport_makefile
-
-import logging
-import ssl
-import six
-import sys
-
-from .. import util
-
-__all__ = ['inject_into_urllib3', 'extract_from_urllib3']
-
-# SNI always works.
-HAS_SNI = True
-
-# Map from urllib3 to PyOpenSSL compatible parameter-values.
-_openssl_versions = {
- ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD,
- ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
-}
-
-if hasattr(ssl, 'PROTOCOL_TLSv1_1') and hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'):
- _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD
-
-if hasattr(ssl, 'PROTOCOL_TLSv1_2') and hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'):
- _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD
-
-try:
- _openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD})
-except AttributeError:
- pass
-
-_stdlib_to_openssl_verify = {
- ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
- ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,
- ssl.CERT_REQUIRED:
- OpenSSL.SSL.VERIFY_PEER + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
-}
-_openssl_to_stdlib_verify = dict(
- (v, k) for k, v in _stdlib_to_openssl_verify.items()
-)
-
-# OpenSSL will only write 16K at a time
-SSL_WRITE_BLOCKSIZE = 16384
-
-orig_util_HAS_SNI = util.HAS_SNI
-orig_util_SSLContext = util.ssl_.SSLContext
-
-
-log = logging.getLogger(__name__)
-
-
-def inject_into_urllib3():
- 'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.'
-
- _validate_dependencies_met()
-
- util.ssl_.SSLContext = PyOpenSSLContext
- util.HAS_SNI = HAS_SNI
- util.ssl_.HAS_SNI = HAS_SNI
- util.IS_PYOPENSSL = True
- util.ssl_.IS_PYOPENSSL = True
-
-
-def extract_from_urllib3():
- 'Undo monkey-patching by :func:`inject_into_urllib3`.'
-
- util.ssl_.SSLContext = orig_util_SSLContext
- util.HAS_SNI = orig_util_HAS_SNI
- util.ssl_.HAS_SNI = orig_util_HAS_SNI
- util.IS_PYOPENSSL = False
- util.ssl_.IS_PYOPENSSL = False
-
-
-def _validate_dependencies_met():
- """
- Verifies that PyOpenSSL's package-level dependencies have been met.
- Throws `ImportError` if they are not met.
- """
- # Method added in `cryptography==1.1`; not available in older versions
- from cryptography.x509.extensions import Extensions
- if getattr(Extensions, "get_extension_for_class", None) is None:
- raise ImportError("'cryptography' module missing required functionality. "
- "Try upgrading to v1.3.4 or newer.")
-
- # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509
- # attribute is only present on those versions.
- from OpenSSL.crypto import X509
- x509 = X509()
- if getattr(x509, "_x509", None) is None:
- raise ImportError("'pyOpenSSL' module missing required functionality. "
- "Try upgrading to v0.14 or newer.")
-
-
-def _dnsname_to_stdlib(name):
- """
- Converts a dNSName SubjectAlternativeName field to the form used by the
- standard library on the given Python version.
-
- Cryptography produces a dNSName as a unicode string that was idna-decoded
- from ASCII bytes. We need to idna-encode that string to get it back, and
- then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib
- uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8).
- """
- def idna_encode(name):
- """
- Borrowed wholesale from the Python Cryptography Project. It turns out
- that we can't just safely call `idna.encode`: it can explode for
- wildcard names. This avoids that problem.
- """
- import idna
-
- for prefix in [u'*.', u'.']:
- if name.startswith(prefix):
- name = name[len(prefix):]
- return prefix.encode('ascii') + idna.encode(name)
- return idna.encode(name)
-
- name = idna_encode(name)
- if sys.version_info >= (3, 0):
- name = name.decode('utf-8')
- return name
-
-
-def get_subj_alt_name(peer_cert):
- """
- Given an PyOpenSSL certificate, provides all the subject alternative names.
- """
- # Pass the cert to cryptography, which has much better APIs for this.
- # This is technically using private APIs, but should work across all
- # relevant versions until PyOpenSSL gets something proper for this.
- cert = _Certificate(openssl_backend, peer_cert._x509)
-
- # We want to find the SAN extension. Ask Cryptography to locate it (it's
- # faster than looping in Python)
- try:
- ext = cert.extensions.get_extension_for_class(
- x509.SubjectAlternativeName
- ).value
- except x509.ExtensionNotFound:
- # No such extension, return the empty list.
- return []
- except (x509.DuplicateExtension, x509.UnsupportedExtension,
- x509.UnsupportedGeneralNameType, UnicodeError) as e:
- # A problem has been found with the quality of the certificate. Assume
- # no SAN field is present.
- log.warning(
- "A problem was encountered with the certificate that prevented "
- "urllib3 from finding the SubjectAlternativeName field. This can "
- "affect certificate validation. The error was %s",
- e,
- )
- return []
-
- # We want to return dNSName and iPAddress fields. We need to cast the IPs
- # back to strings because the match_hostname function wants them as
- # strings.
- # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8
- # decoded. This is pretty frustrating, but that's what the standard library
- # does with certificates, and so we need to attempt to do the same.
- names = [
- ('DNS', _dnsname_to_stdlib(name))
- for name in ext.get_values_for_type(x509.DNSName)
- ]
- names.extend(
- ('IP Address', str(name))
- for name in ext.get_values_for_type(x509.IPAddress)
- )
-
- return names
-
-
-class WrappedSocket(object):
- '''API-compatibility wrapper for Python OpenSSL's Connection-class.
-
- Note: _makefile_refs, _drop() and _reuse() are needed for the garbage
- collector of pypy.
- '''
-
- def __init__(self, connection, socket, suppress_ragged_eofs=True):
- self.connection = connection
- self.socket = socket
- self.suppress_ragged_eofs = suppress_ragged_eofs
- self._makefile_refs = 0
- self._closed = False
-
- def fileno(self):
- return self.socket.fileno()
-
- # Copy-pasted from Python 3.5 source code
- def _decref_socketios(self):
- if self._makefile_refs > 0:
- self._makefile_refs -= 1
- if self._closed:
- self.close()
-
- def recv(self, *args, **kwargs):
- try:
- data = self.connection.recv(*args, **kwargs)
- except OpenSSL.SSL.SysCallError as e:
- if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'):
- return b''
- else:
- raise SocketError(str(e))
- except OpenSSL.SSL.ZeroReturnError as e:
- if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
- return b''
- else:
- raise
- except OpenSSL.SSL.WantReadError:
- rd = util.wait_for_read(self.socket, self.socket.gettimeout())
- if not rd:
- raise timeout('The read operation timed out')
- else:
- return self.recv(*args, **kwargs)
- else:
- return data
-
- def recv_into(self, *args, **kwargs):
- try:
- return self.connection.recv_into(*args, **kwargs)
- except OpenSSL.SSL.SysCallError as e:
- if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'):
- return 0
- else:
- raise SocketError(str(e))
- except OpenSSL.SSL.ZeroReturnError as e:
- if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
- return 0
- else:
- raise
- except OpenSSL.SSL.WantReadError:
- rd = util.wait_for_read(self.socket, self.socket.gettimeout())
- if not rd:
- raise timeout('The read operation timed out')
- else:
- return self.recv_into(*args, **kwargs)
-
- def settimeout(self, timeout):
- return self.socket.settimeout(timeout)
-
- def _send_until_done(self, data):
- while True:
- try:
- return self.connection.send(data)
- except OpenSSL.SSL.WantWriteError:
- wr = util.wait_for_write(self.socket, self.socket.gettimeout())
- if not wr:
- raise timeout()
- continue
-
- def sendall(self, data):
- total_sent = 0
- while total_sent < len(data):
- sent = self._send_until_done(data[total_sent:total_sent + SSL_WRITE_BLOCKSIZE])
- total_sent += sent
-
- def shutdown(self):
- # FIXME rethrow compatible exceptions should we ever use this
- self.connection.shutdown()
-
- def close(self):
- if self._makefile_refs < 1:
- try:
- self._closed = True
- return self.connection.close()
- except OpenSSL.SSL.Error:
- return
- else:
- self._makefile_refs -= 1
-
- def getpeercert(self, binary_form=False):
- x509 = self.connection.get_peer_certificate()
-
- if not x509:
- return x509
-
- if binary_form:
- return OpenSSL.crypto.dump_certificate(
- OpenSSL.crypto.FILETYPE_ASN1,
- x509)
-
- return {
- 'subject': (
- (('commonName', x509.get_subject().CN),),
- ),
- 'subjectAltName': get_subj_alt_name(x509)
- }
-
- def _reuse(self):
- self._makefile_refs += 1
-
- def _drop(self):
- if self._makefile_refs < 1:
- self.close()
- else:
- self._makefile_refs -= 1
-
-
-if _fileobject: # Platform-specific: Python 2
- def makefile(self, mode, bufsize=-1):
- self._makefile_refs += 1
- return _fileobject(self, mode, bufsize, close=True)
-else: # Platform-specific: Python 3
- makefile = backport_makefile
-
-WrappedSocket.makefile = makefile
-
-
-class PyOpenSSLContext(object):
- """
- I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible
- for translating the interface of the standard library ``SSLContext`` object
- to calls into PyOpenSSL.
- """
- def __init__(self, protocol):
- self.protocol = _openssl_versions[protocol]
- self._ctx = OpenSSL.SSL.Context(self.protocol)
- self._options = 0
- self.check_hostname = False
-
- @property
- def options(self):
- return self._options
-
- @options.setter
- def options(self, value):
- self._options = value
- self._ctx.set_options(value)
-
- @property
- def verify_mode(self):
- return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()]
-
- @verify_mode.setter
- def verify_mode(self, value):
- self._ctx.set_verify(
- _stdlib_to_openssl_verify[value],
- _verify_callback
- )
-
- def set_default_verify_paths(self):
- self._ctx.set_default_verify_paths()
-
- def set_ciphers(self, ciphers):
- if isinstance(ciphers, six.text_type):
- ciphers = ciphers.encode('utf-8')
- self._ctx.set_cipher_list(ciphers)
-
- def load_verify_locations(self, cafile=None, capath=None, cadata=None):
- if cafile is not None:
- cafile = cafile.encode('utf-8')
- if capath is not None:
- capath = capath.encode('utf-8')
- self._ctx.load_verify_locations(cafile, capath)
- if cadata is not None:
- self._ctx.load_verify_locations(BytesIO(cadata))
-
- def load_cert_chain(self, certfile, keyfile=None, password=None):
- self._ctx.use_certificate_file(certfile)
- if password is not None:
- self._ctx.set_passwd_cb(lambda max_length, prompt_twice, userdata: password)
- self._ctx.use_privatekey_file(keyfile or certfile)
-
- def wrap_socket(self, sock, server_side=False,
- do_handshake_on_connect=True, suppress_ragged_eofs=True,
- server_hostname=None):
- cnx = OpenSSL.SSL.Connection(self._ctx, sock)
-
- if isinstance(server_hostname, six.text_type): # Platform-specific: Python 3
- server_hostname = server_hostname.encode('utf-8')
-
- if server_hostname is not None:
- cnx.set_tlsext_host_name(server_hostname)
-
- cnx.set_connect_state()
-
- while True:
- try:
- cnx.do_handshake()
- except OpenSSL.SSL.WantReadError:
- rd = util.wait_for_read(sock, sock.gettimeout())
- if not rd:
- raise timeout('select timed out')
- continue
- except OpenSSL.SSL.Error as e:
- raise ssl.SSLError('bad handshake: %r' % e)
- break
-
- return WrappedSocket(cnx, sock)
-
-
-def _verify_callback(cnx, x509, err_no, err_depth, return_code):
- return err_no == 0
+"""
+SSL with SNI_-support for Python 2. Follow these instructions if you would
+like to verify SSL certificates in Python 2. Note, the default libraries do
+*not* do certificate checking; you need to do additional work to validate
+certificates yourself.
+
+This needs the following packages installed:
+
+* pyOpenSSL (tested with 16.0.0)
+* cryptography (minimum 1.3.4, from pyopenssl)
+* idna (minimum 2.0, from cryptography)
+
+However, pyopenssl depends on cryptography, which depends on idna, so while we
+use all three directly here we end up having relatively few packages required.
+
+You can install them with the following command:
+
+ pip install pyopenssl cryptography idna
+
+To activate certificate checking, call
+:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code
+before you begin making HTTP requests. This can be done in a ``sitecustomize``
+module, or at any other time before your application begins using ``urllib3``,
+like this::
+
+ try:
+ import urllib3.contrib.pyopenssl
+ urllib3.contrib.pyopenssl.inject_into_urllib3()
+ except ImportError:
+ pass
+
+Now you can use :mod:`urllib3` as you normally would, and it will support SNI
+when the required modules are installed.
+
+Activating this module also has the positive side effect of disabling SSL/TLS
+compression in Python 2 (see `CRIME attack`_).
+
+If you want to configure the default list of supported cipher suites, you can
+set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable.
+
+.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication
+.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit)
+"""
+from __future__ import absolute_import
+
+import OpenSSL.SSL
+from cryptography import x509
+from cryptography.hazmat.backends.openssl import backend as openssl_backend
+from cryptography.hazmat.backends.openssl.x509 import _Certificate
+
+from socket import timeout, error as SocketError
+from io import BytesIO
+
+try: # Platform-specific: Python 2
+ from socket import _fileobject
+except ImportError: # Platform-specific: Python 3
+ _fileobject = None
+ from ..packages.backports.makefile import backport_makefile
+
+import logging
+import ssl
+import six
+import sys
+
+from .. import util
+
+__all__ = ['inject_into_urllib3', 'extract_from_urllib3']
+
+# SNI always works.
+HAS_SNI = True
+
+# Map from urllib3 to PyOpenSSL compatible parameter-values.
+_openssl_versions = {
+ ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD,
+ ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
+}
+
+if hasattr(ssl, 'PROTOCOL_TLSv1_1') and hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'):
+ _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD
+
+if hasattr(ssl, 'PROTOCOL_TLSv1_2') and hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'):
+ _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD
+
+try:
+ _openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD})
+except AttributeError:
+ pass
+
+_stdlib_to_openssl_verify = {
+ ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
+ ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,
+ ssl.CERT_REQUIRED:
+ OpenSSL.SSL.VERIFY_PEER + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
+}
+_openssl_to_stdlib_verify = dict(
+ (v, k) for k, v in _stdlib_to_openssl_verify.items()
+)
+
+# OpenSSL will only write 16K at a time
+SSL_WRITE_BLOCKSIZE = 16384
+
+orig_util_HAS_SNI = util.HAS_SNI
+orig_util_SSLContext = util.ssl_.SSLContext
+
+
+log = logging.getLogger(__name__)
+
+
+def inject_into_urllib3():
+ 'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.'
+
+ _validate_dependencies_met()
+
+ util.ssl_.SSLContext = PyOpenSSLContext
+ util.HAS_SNI = HAS_SNI
+ util.ssl_.HAS_SNI = HAS_SNI
+ util.IS_PYOPENSSL = True
+ util.ssl_.IS_PYOPENSSL = True
+
+
+def extract_from_urllib3():
+ 'Undo monkey-patching by :func:`inject_into_urllib3`.'
+
+ util.ssl_.SSLContext = orig_util_SSLContext
+ util.HAS_SNI = orig_util_HAS_SNI
+ util.ssl_.HAS_SNI = orig_util_HAS_SNI
+ util.IS_PYOPENSSL = False
+ util.ssl_.IS_PYOPENSSL = False
+
+
+def _validate_dependencies_met():
+ """
+ Verifies that PyOpenSSL's package-level dependencies have been met.
+ Throws `ImportError` if they are not met.
+ """
+ # Method added in `cryptography==1.1`; not available in older versions
+ from cryptography.x509.extensions import Extensions
+ if getattr(Extensions, "get_extension_for_class", None) is None:
+ raise ImportError("'cryptography' module missing required functionality. "
+ "Try upgrading to v1.3.4 or newer.")
+
+ # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509
+ # attribute is only present on those versions.
+ from OpenSSL.crypto import X509
+ x509 = X509()
+ if getattr(x509, "_x509", None) is None:
+ raise ImportError("'pyOpenSSL' module missing required functionality. "
+ "Try upgrading to v0.14 or newer.")
+
+
+def _dnsname_to_stdlib(name):
+ """
+ Converts a dNSName SubjectAlternativeName field to the form used by the
+ standard library on the given Python version.
+
+ Cryptography produces a dNSName as a unicode string that was idna-decoded
+ from ASCII bytes. We need to idna-encode that string to get it back, and
+ then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib
+ uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8).
+ """
+ def idna_encode(name):
+ """
+ Borrowed wholesale from the Python Cryptography Project. It turns out
+ that we can't just safely call `idna.encode`: it can explode for
+ wildcard names. This avoids that problem.
+ """
+ import idna
+
+ for prefix in [u'*.', u'.']:
+ if name.startswith(prefix):
+ name = name[len(prefix):]
+ return prefix.encode('ascii') + idna.encode(name)
+ return idna.encode(name)
+
+ name = idna_encode(name)
+ if sys.version_info >= (3, 0):
+ name = name.decode('utf-8')
+ return name
+
+
+def get_subj_alt_name(peer_cert):
+ """
+ Given an PyOpenSSL certificate, provides all the subject alternative names.
+ """
+ # Pass the cert to cryptography, which has much better APIs for this.
+ # This is technically using private APIs, but should work across all
+ # relevant versions until PyOpenSSL gets something proper for this.
+ cert = _Certificate(openssl_backend, peer_cert._x509)
+
+ # We want to find the SAN extension. Ask Cryptography to locate it (it's
+ # faster than looping in Python)
+ try:
+ ext = cert.extensions.get_extension_for_class(
+ x509.SubjectAlternativeName
+ ).value
+ except x509.ExtensionNotFound:
+ # No such extension, return the empty list.
+ return []
+ except (x509.DuplicateExtension, x509.UnsupportedExtension,
+ x509.UnsupportedGeneralNameType, UnicodeError) as e:
+ # A problem has been found with the quality of the certificate. Assume
+ # no SAN field is present.
+ log.warning(
+ "A problem was encountered with the certificate that prevented "
+ "urllib3 from finding the SubjectAlternativeName field. This can "
+ "affect certificate validation. The error was %s",
+ e,
+ )
+ return []
+
+ # We want to return dNSName and iPAddress fields. We need to cast the IPs
+ # back to strings because the match_hostname function wants them as
+ # strings.
+ # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8
+ # decoded. This is pretty frustrating, but that's what the standard library
+ # does with certificates, and so we need to attempt to do the same.
+ names = [
+ ('DNS', _dnsname_to_stdlib(name))
+ for name in ext.get_values_for_type(x509.DNSName)
+ ]
+ names.extend(
+ ('IP Address', str(name))
+ for name in ext.get_values_for_type(x509.IPAddress)
+ )
+
+ return names
+
+
+class WrappedSocket(object):
+ '''API-compatibility wrapper for Python OpenSSL's Connection-class.
+
+ Note: _makefile_refs, _drop() and _reuse() are needed for the garbage
+ collector of pypy.
+ '''
+
+ def __init__(self, connection, socket, suppress_ragged_eofs=True):
+ self.connection = connection
+ self.socket = socket
+ self.suppress_ragged_eofs = suppress_ragged_eofs
+ self._makefile_refs = 0
+ self._closed = False
+
+ def fileno(self):
+ return self.socket.fileno()
+
+ # Copy-pasted from Python 3.5 source code
+ def _decref_socketios(self):
+ if self._makefile_refs > 0:
+ self._makefile_refs -= 1
+ if self._closed:
+ self.close()
+
+ def recv(self, *args, **kwargs):
+ try:
+ data = self.connection.recv(*args, **kwargs)
+ except OpenSSL.SSL.SysCallError as e:
+ if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'):
+ return b''
+ else:
+ raise SocketError(str(e))
+ except OpenSSL.SSL.ZeroReturnError as e:
+ if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
+ return b''
+ else:
+ raise
+ except OpenSSL.SSL.WantReadError:
+ rd = util.wait_for_read(self.socket, self.socket.gettimeout())
+ if not rd:
+ raise timeout('The read operation timed out')
+ else:
+ return self.recv(*args, **kwargs)
+ else:
+ return data
+
+ def recv_into(self, *args, **kwargs):
+ try:
+ return self.connection.recv_into(*args, **kwargs)
+ except OpenSSL.SSL.SysCallError as e:
+ if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'):
+ return 0
+ else:
+ raise SocketError(str(e))
+ except OpenSSL.SSL.ZeroReturnError as e:
+ if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
+ return 0
+ else:
+ raise
+ except OpenSSL.SSL.WantReadError:
+ rd = util.wait_for_read(self.socket, self.socket.gettimeout())
+ if not rd:
+ raise timeout('The read operation timed out')
+ else:
+ return self.recv_into(*args, **kwargs)
+
+ def settimeout(self, timeout):
+ return self.socket.settimeout(timeout)
+
+ def _send_until_done(self, data):
+ while True:
+ try:
+ return self.connection.send(data)
+ except OpenSSL.SSL.WantWriteError:
+ wr = util.wait_for_write(self.socket, self.socket.gettimeout())
+ if not wr:
+ raise timeout()
+ continue
+
+ def sendall(self, data):
+ total_sent = 0
+ while total_sent < len(data):
+ sent = self._send_until_done(data[total_sent:total_sent + SSL_WRITE_BLOCKSIZE])
+ total_sent += sent
+
+ def shutdown(self):
+ # FIXME rethrow compatible exceptions should we ever use this
+ self.connection.shutdown()
+
+ def close(self):
+ if self._makefile_refs < 1:
+ try:
+ self._closed = True
+ return self.connection.close()
+ except OpenSSL.SSL.Error:
+ return
+ else:
+ self._makefile_refs -= 1
+
+ def getpeercert(self, binary_form=False):
+ x509 = self.connection.get_peer_certificate()
+
+ if not x509:
+ return x509
+
+ if binary_form:
+ return OpenSSL.crypto.dump_certificate(
+ OpenSSL.crypto.FILETYPE_ASN1,
+ x509)
+
+ return {
+ 'subject': (
+ (('commonName', x509.get_subject().CN),),
+ ),
+ 'subjectAltName': get_subj_alt_name(x509)
+ }
+
+ def _reuse(self):
+ self._makefile_refs += 1
+
+ def _drop(self):
+ if self._makefile_refs < 1:
+ self.close()
+ else:
+ self._makefile_refs -= 1
+
+
+if _fileobject: # Platform-specific: Python 2
+ def makefile(self, mode, bufsize=-1):
+ self._makefile_refs += 1
+ return _fileobject(self, mode, bufsize, close=True)
+else: # Platform-specific: Python 3
+ makefile = backport_makefile
+
+WrappedSocket.makefile = makefile
+
+
+class PyOpenSSLContext(object):
+ """
+ I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible
+ for translating the interface of the standard library ``SSLContext`` object
+ to calls into PyOpenSSL.
+ """
+ def __init__(self, protocol):
+ self.protocol = _openssl_versions[protocol]
+ self._ctx = OpenSSL.SSL.Context(self.protocol)
+ self._options = 0
+ self.check_hostname = False
+
+ @property
+ def options(self):
+ return self._options
+
+ @options.setter
+ def options(self, value):
+ self._options = value
+ self._ctx.set_options(value)
+
+ @property
+ def verify_mode(self):
+ return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()]
+
+ @verify_mode.setter
+ def verify_mode(self, value):
+ self._ctx.set_verify(
+ _stdlib_to_openssl_verify[value],
+ _verify_callback
+ )
+
+ def set_default_verify_paths(self):
+ self._ctx.set_default_verify_paths()
+
+ def set_ciphers(self, ciphers):
+ if isinstance(ciphers, six.text_type):
+ ciphers = ciphers.encode('utf-8')
+ self._ctx.set_cipher_list(ciphers)
+
+ def load_verify_locations(self, cafile=None, capath=None, cadata=None):
+ if cafile is not None:
+ cafile = cafile.encode('utf-8')
+ if capath is not None:
+ capath = capath.encode('utf-8')
+ self._ctx.load_verify_locations(cafile, capath)
+ if cadata is not None:
+ self._ctx.load_verify_locations(BytesIO(cadata))
+
+ def load_cert_chain(self, certfile, keyfile=None, password=None):
+ self._ctx.use_certificate_file(certfile)
+ if password is not None:
+ self._ctx.set_passwd_cb(lambda max_length, prompt_twice, userdata: password)
+ self._ctx.use_privatekey_file(keyfile or certfile)
+
+ def wrap_socket(self, sock, server_side=False,
+ do_handshake_on_connect=True, suppress_ragged_eofs=True,
+ server_hostname=None):
+ cnx = OpenSSL.SSL.Connection(self._ctx, sock)
+
+ if isinstance(server_hostname, six.text_type): # Platform-specific: Python 3
+ server_hostname = server_hostname.encode('utf-8')
+
+ if server_hostname is not None:
+ cnx.set_tlsext_host_name(server_hostname)
+
+ cnx.set_connect_state()
+
+ while True:
+ try:
+ cnx.do_handshake()
+ except OpenSSL.SSL.WantReadError:
+ rd = util.wait_for_read(sock, sock.gettimeout())
+ if not rd:
+ raise timeout('select timed out')
+ continue
+ except OpenSSL.SSL.Error as e:
+ raise ssl.SSLError('bad handshake: %r' % e)
+ break
+
+ return WrappedSocket(cnx, sock)
+
+
+def _verify_callback(cnx, x509, err_no, err_depth, return_code):
+ return err_no == 0
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/socks.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/socks.py
index a2d1b46..811e312 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/socks.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/contrib/socks.py
@@ -1,178 +1,192 @@
-# -*- coding: utf-8 -*-
-"""
-This module contains provisional support for SOCKS proxies from within
-urllib3. This module supports SOCKS4 (specifically the SOCKS4A variant) and
-SOCKS5. To enable its functionality, either install PySocks or install this
-module with the ``socks`` extra.
-
-The SOCKS implementation supports the full range of urllib3 features. It also
-supports the following SOCKS features:
-
-- SOCKS4
-- SOCKS4a
-- SOCKS5
-- Usernames and passwords for the SOCKS proxy
-
-Known Limitations:
-
-- Currently PySocks does not support contacting remote websites via literal
- IPv6 addresses. Any such connection attempt will fail. You must use a domain
- name.
-- Currently PySocks does not support IPv6 connections to the SOCKS proxy. Any
- such connection attempt will fail.
-"""
-from __future__ import absolute_import
-
-try:
- import socks
-except ImportError:
- import warnings
- from ..exceptions import DependencyWarning
-
- warnings.warn((
- 'SOCKS support in urllib3 requires the installation of optional '
- 'dependencies: specifically, PySocks. For more information, see '
- 'https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies'
- ),
- DependencyWarning
- )
- raise
-
-from socket import error as SocketError, timeout as SocketTimeout
-
-from ..connection import (
- HTTPConnection, HTTPSConnection
-)
-from ..connectionpool import (
- HTTPConnectionPool, HTTPSConnectionPool
-)
-from ..exceptions import ConnectTimeoutError, NewConnectionError
-from ..poolmanager import PoolManager
-from ..util.url import parse_url
-
-try:
- import ssl
-except ImportError:
- ssl = None
-
-
-class SOCKSConnection(HTTPConnection):
- """
- A plain-text HTTP connection that connects via a SOCKS proxy.
- """
- def __init__(self, *args, **kwargs):
- self._socks_options = kwargs.pop('_socks_options')
- super(SOCKSConnection, self).__init__(*args, **kwargs)
-
- def _new_conn(self):
- """
- Establish a new connection via the SOCKS proxy.
- """
- extra_kw = {}
- if self.source_address:
- extra_kw['source_address'] = self.source_address
-
- if self.socket_options:
- extra_kw['socket_options'] = self.socket_options
-
- try:
- conn = socks.create_connection(
- (self.host, self.port),
- proxy_type=self._socks_options['socks_version'],
- proxy_addr=self._socks_options['proxy_host'],
- proxy_port=self._socks_options['proxy_port'],
- proxy_username=self._socks_options['username'],
- proxy_password=self._socks_options['password'],
- timeout=self.timeout,
- **extra_kw
- )
-
- except SocketTimeout as e:
- raise ConnectTimeoutError(
- self, "Connection to %s timed out. (connect timeout=%s)" %
- (self.host, self.timeout))
-
- except socks.ProxyError as e:
- # This is fragile as hell, but it seems to be the only way to raise
- # useful errors here.
- if e.socket_err:
- error = e.socket_err
- if isinstance(error, SocketTimeout):
- raise ConnectTimeoutError(
- self,
- "Connection to %s timed out. (connect timeout=%s)" %
- (self.host, self.timeout)
- )
- else:
- raise NewConnectionError(
- self,
- "Failed to establish a new connection: %s" % error
- )
- else:
- raise NewConnectionError(
- self,
- "Failed to establish a new connection: %s" % e
- )
-
- except SocketError as e: # Defensive: PySocks should catch all these.
- raise NewConnectionError(
- self, "Failed to establish a new connection: %s" % e)
-
- return conn
-
-
-# We don't need to duplicate the Verified/Unverified distinction from
-# urllib3/connection.py here because the HTTPSConnection will already have been
-# correctly set to either the Verified or Unverified form by that module. This
-# means the SOCKSHTTPSConnection will automatically be the correct type.
-class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection):
- pass
-
-
-class SOCKSHTTPConnectionPool(HTTPConnectionPool):
- ConnectionCls = SOCKSConnection
-
-
-class SOCKSHTTPSConnectionPool(HTTPSConnectionPool):
- ConnectionCls = SOCKSHTTPSConnection
-
-
-class SOCKSProxyManager(PoolManager):
- """
- A version of the urllib3 ProxyManager that routes connections via the
- defined SOCKS proxy.
- """
- pool_classes_by_scheme = {
- 'http': SOCKSHTTPConnectionPool,
- 'https': SOCKSHTTPSConnectionPool,
- }
-
- def __init__(self, proxy_url, username=None, password=None,
- num_pools=10, headers=None, **connection_pool_kw):
- parsed = parse_url(proxy_url)
-
- if parsed.scheme == 'socks5':
- socks_version = socks.PROXY_TYPE_SOCKS5
- elif parsed.scheme == 'socks4':
- socks_version = socks.PROXY_TYPE_SOCKS4
- else:
- raise ValueError(
- "Unable to determine SOCKS version from %s" % proxy_url
- )
-
- self.proxy_url = proxy_url
-
- socks_options = {
- 'socks_version': socks_version,
- 'proxy_host': parsed.host,
- 'proxy_port': parsed.port,
- 'username': username,
- 'password': password,
- }
- connection_pool_kw['_socks_options'] = socks_options
-
- super(SOCKSProxyManager, self).__init__(
- num_pools, headers, **connection_pool_kw
- )
-
- self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme
+# -*- coding: utf-8 -*-
+"""
+This module contains provisional support for SOCKS proxies from within
+urllib3. This module supports SOCKS4 (specifically the SOCKS4A variant) and
+SOCKS5. To enable its functionality, either install PySocks or install this
+module with the ``socks`` extra.
+
+The SOCKS implementation supports the full range of urllib3 features. It also
+supports the following SOCKS features:
+
+- SOCKS4
+- SOCKS4a
+- SOCKS5
+- Usernames and passwords for the SOCKS proxy
+
+Known Limitations:
+
+- Currently PySocks does not support contacting remote websites via literal
+ IPv6 addresses. Any such connection attempt will fail. You must use a domain
+ name.
+- Currently PySocks does not support IPv6 connections to the SOCKS proxy. Any
+ such connection attempt will fail.
+"""
+from __future__ import absolute_import
+
+try:
+ import socks
+except ImportError:
+ import warnings
+ from ..exceptions import DependencyWarning
+
+ warnings.warn((
+ 'SOCKS support in urllib3 requires the installation of optional '
+ 'dependencies: specifically, PySocks. For more information, see '
+ 'https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies'
+ ),
+ DependencyWarning
+ )
+ raise
+
+from socket import error as SocketError, timeout as SocketTimeout
+
+from ..connection import (
+ HTTPConnection, HTTPSConnection
+)
+from ..connectionpool import (
+ HTTPConnectionPool, HTTPSConnectionPool
+)
+from ..exceptions import ConnectTimeoutError, NewConnectionError
+from ..poolmanager import PoolManager
+from ..util.url import parse_url
+
+try:
+ import ssl
+except ImportError:
+ ssl = None
+
+
+class SOCKSConnection(HTTPConnection):
+ """
+ A plain-text HTTP connection that connects via a SOCKS proxy.
+ """
+ def __init__(self, *args, **kwargs):
+ self._socks_options = kwargs.pop('_socks_options')
+ super(SOCKSConnection, self).__init__(*args, **kwargs)
+
+ def _new_conn(self):
+ """
+ Establish a new connection via the SOCKS proxy.
+ """
+ extra_kw = {}
+ if self.source_address:
+ extra_kw['source_address'] = self.source_address
+
+ if self.socket_options:
+ extra_kw['socket_options'] = self.socket_options
+
+ try:
+ conn = socks.create_connection(
+ (self.host, self.port),
+ proxy_type=self._socks_options['socks_version'],
+ proxy_addr=self._socks_options['proxy_host'],
+ proxy_port=self._socks_options['proxy_port'],
+ proxy_username=self._socks_options['username'],
+ proxy_password=self._socks_options['password'],
+ proxy_rdns=self._socks_options['rdns'],
+ timeout=self.timeout,
+ **extra_kw
+ )
+
+ except SocketTimeout as e:
+ raise ConnectTimeoutError(
+ self, "Connection to %s timed out. (connect timeout=%s)" %
+ (self.host, self.timeout))
+
+ except socks.ProxyError as e:
+ # This is fragile as hell, but it seems to be the only way to raise
+ # useful errors here.
+ if e.socket_err:
+ error = e.socket_err
+ if isinstance(error, SocketTimeout):
+ raise ConnectTimeoutError(
+ self,
+ "Connection to %s timed out. (connect timeout=%s)" %
+ (self.host, self.timeout)
+ )
+ else:
+ raise NewConnectionError(
+ self,
+ "Failed to establish a new connection: %s" % error
+ )
+ else:
+ raise NewConnectionError(
+ self,
+ "Failed to establish a new connection: %s" % e
+ )
+
+ except SocketError as e: # Defensive: PySocks should catch all these.
+ raise NewConnectionError(
+ self, "Failed to establish a new connection: %s" % e)
+
+ return conn
+
+
+# We don't need to duplicate the Verified/Unverified distinction from
+# urllib3/connection.py here because the HTTPSConnection will already have been
+# correctly set to either the Verified or Unverified form by that module. This
+# means the SOCKSHTTPSConnection will automatically be the correct type.
+class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection):
+ pass
+
+
+class SOCKSHTTPConnectionPool(HTTPConnectionPool):
+ ConnectionCls = SOCKSConnection
+
+
+class SOCKSHTTPSConnectionPool(HTTPSConnectionPool):
+ ConnectionCls = SOCKSHTTPSConnection
+
+
+class SOCKSProxyManager(PoolManager):
+ """
+ A version of the urllib3 ProxyManager that routes connections via the
+ defined SOCKS proxy.
+ """
+ pool_classes_by_scheme = {
+ 'http': SOCKSHTTPConnectionPool,
+ 'https': SOCKSHTTPSConnectionPool,
+ }
+
+ def __init__(self, proxy_url, username=None, password=None,
+ num_pools=10, headers=None, **connection_pool_kw):
+ parsed = parse_url(proxy_url)
+
+ if username is None and password is None and parsed.auth is not None:
+ split = parsed.auth.split(':')
+ if len(split) == 2:
+ username, password = split
+ if parsed.scheme == 'socks5':
+ socks_version = socks.PROXY_TYPE_SOCKS5
+ rdns = False
+ elif parsed.scheme == 'socks5h':
+ socks_version = socks.PROXY_TYPE_SOCKS5
+ rdns = True
+ elif parsed.scheme == 'socks4':
+ socks_version = socks.PROXY_TYPE_SOCKS4
+ rdns = False
+ elif parsed.scheme == 'socks4a':
+ socks_version = socks.PROXY_TYPE_SOCKS4
+ rdns = True
+ else:
+ raise ValueError(
+ "Unable to determine SOCKS version from %s" % proxy_url
+ )
+
+ self.proxy_url = proxy_url
+
+ socks_options = {
+ 'socks_version': socks_version,
+ 'proxy_host': parsed.host,
+ 'proxy_port': parsed.port,
+ 'username': username,
+ 'password': password,
+ 'rdns': rdns
+ }
+ connection_pool_kw['_socks_options'] = socks_options
+
+ super(SOCKSProxyManager, self).__init__(
+ num_pools, headers, **connection_pool_kw
+ )
+
+ self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/exceptions.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/exceptions.py
index 670a63e..6c4be58 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/exceptions.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/exceptions.py
@@ -1,246 +1,246 @@
-from __future__ import absolute_import
-from .packages.six.moves.http_client import (
- IncompleteRead as httplib_IncompleteRead
-)
-# Base Exceptions
-
-
-class HTTPError(Exception):
- "Base exception used by this module."
- pass
-
-
-class HTTPWarning(Warning):
- "Base warning used by this module."
- pass
-
-
-class PoolError(HTTPError):
- "Base exception for errors caused within a pool."
- def __init__(self, pool, message):
- self.pool = pool
- HTTPError.__init__(self, "%s: %s" % (pool, message))
-
- def __reduce__(self):
- # For pickling purposes.
- return self.__class__, (None, None)
-
-
-class RequestError(PoolError):
- "Base exception for PoolErrors that have associated URLs."
- def __init__(self, pool, url, message):
- self.url = url
- PoolError.__init__(self, pool, message)
-
- def __reduce__(self):
- # For pickling purposes.
- return self.__class__, (None, self.url, None)
-
-
-class SSLError(HTTPError):
- "Raised when SSL certificate fails in an HTTPS connection."
- pass
-
-
-class ProxyError(HTTPError):
- "Raised when the connection to a proxy fails."
- pass
-
-
-class DecodeError(HTTPError):
- "Raised when automatic decoding based on Content-Type fails."
- pass
-
-
-class ProtocolError(HTTPError):
- "Raised when something unexpected happens mid-request/response."
- pass
-
-
-#: Renamed to ProtocolError but aliased for backwards compatibility.
-ConnectionError = ProtocolError
-
-
-# Leaf Exceptions
-
-class MaxRetryError(RequestError):
- """Raised when the maximum number of retries is exceeded.
-
- :param pool: The connection pool
- :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool`
- :param string url: The requested Url
- :param exceptions.Exception reason: The underlying error
-
- """
-
- def __init__(self, pool, url, reason=None):
- self.reason = reason
-
- message = "Max retries exceeded with url: %s (Caused by %r)" % (
- url, reason)
-
- RequestError.__init__(self, pool, url, message)
-
-
-class HostChangedError(RequestError):
- "Raised when an existing pool gets a request for a foreign host."
-
- def __init__(self, pool, url, retries=3):
- message = "Tried to open a foreign host with url: %s" % url
- RequestError.__init__(self, pool, url, message)
- self.retries = retries
-
-
-class TimeoutStateError(HTTPError):
- """ Raised when passing an invalid state to a timeout """
- pass
-
-
-class TimeoutError(HTTPError):
- """ Raised when a socket timeout error occurs.
-
- Catching this error will catch both :exc:`ReadTimeoutErrors
- ` and :exc:`ConnectTimeoutErrors `.
- """
- pass
-
-
-class ReadTimeoutError(TimeoutError, RequestError):
- "Raised when a socket timeout occurs while receiving data from a server"
- pass
-
-
-# This timeout error does not have a URL attached and needs to inherit from the
-# base HTTPError
-class ConnectTimeoutError(TimeoutError):
- "Raised when a socket timeout occurs while connecting to a server"
- pass
-
-
-class NewConnectionError(ConnectTimeoutError, PoolError):
- "Raised when we fail to establish a new connection. Usually ECONNREFUSED."
- pass
-
-
-class EmptyPoolError(PoolError):
- "Raised when a pool runs out of connections and no more are allowed."
- pass
-
-
-class ClosedPoolError(PoolError):
- "Raised when a request enters a pool after the pool has been closed."
- pass
-
-
-class LocationValueError(ValueError, HTTPError):
- "Raised when there is something wrong with a given URL input."
- pass
-
-
-class LocationParseError(LocationValueError):
- "Raised when get_host or similar fails to parse the URL input."
-
- def __init__(self, location):
- message = "Failed to parse: %s" % location
- HTTPError.__init__(self, message)
-
- self.location = location
-
-
-class ResponseError(HTTPError):
- "Used as a container for an error reason supplied in a MaxRetryError."
- GENERIC_ERROR = 'too many error responses'
- SPECIFIC_ERROR = 'too many {status_code} error responses'
-
-
-class SecurityWarning(HTTPWarning):
- "Warned when perfoming security reducing actions"
- pass
-
-
-class SubjectAltNameWarning(SecurityWarning):
- "Warned when connecting to a host with a certificate missing a SAN."
- pass
-
-
-class InsecureRequestWarning(SecurityWarning):
- "Warned when making an unverified HTTPS request."
- pass
-
-
-class SystemTimeWarning(SecurityWarning):
- "Warned when system time is suspected to be wrong"
- pass
-
-
-class InsecurePlatformWarning(SecurityWarning):
- "Warned when certain SSL configuration is not available on a platform."
- pass
-
-
-class SNIMissingWarning(HTTPWarning):
- "Warned when making a HTTPS request without SNI available."
- pass
-
-
-class DependencyWarning(HTTPWarning):
- """
- Warned when an attempt is made to import a module with missing optional
- dependencies.
- """
- pass
-
-
-class ResponseNotChunked(ProtocolError, ValueError):
- "Response needs to be chunked in order to read it as chunks."
- pass
-
-
-class BodyNotHttplibCompatible(HTTPError):
- """
- Body should be httplib.HTTPResponse like (have an fp attribute which
- returns raw chunks) for read_chunked().
- """
- pass
-
-
-class IncompleteRead(HTTPError, httplib_IncompleteRead):
- """
- Response length doesn't match expected Content-Length
-
- Subclass of http_client.IncompleteRead to allow int value
- for `partial` to avoid creating large objects on streamed
- reads.
- """
- def __init__(self, partial, expected):
- super(IncompleteRead, self).__init__(partial, expected)
-
- def __repr__(self):
- return ('IncompleteRead(%i bytes read, '
- '%i more expected)' % (self.partial, self.expected))
-
-
-class InvalidHeader(HTTPError):
- "The header provided was somehow invalid."
- pass
-
-
-class ProxySchemeUnknown(AssertionError, ValueError):
- "ProxyManager does not support the supplied scheme"
- # TODO(t-8ch): Stop inheriting from AssertionError in v2.0.
-
- def __init__(self, scheme):
- message = "Not supported proxy scheme %s" % scheme
- super(ProxySchemeUnknown, self).__init__(message)
-
-
-class HeaderParsingError(HTTPError):
- "Raised by assert_header_parsing, but we convert it to a log.warning statement."
- def __init__(self, defects, unparsed_data):
- message = '%s, unparsed data: %r' % (defects or 'Unknown', unparsed_data)
- super(HeaderParsingError, self).__init__(message)
-
-
-class UnrewindableBodyError(HTTPError):
- "urllib3 encountered an error when trying to rewind a body"
- pass
+from __future__ import absolute_import
+from .packages.six.moves.http_client import (
+ IncompleteRead as httplib_IncompleteRead
+)
+# Base Exceptions
+
+
+class HTTPError(Exception):
+ "Base exception used by this module."
+ pass
+
+
+class HTTPWarning(Warning):
+ "Base warning used by this module."
+ pass
+
+
+class PoolError(HTTPError):
+ "Base exception for errors caused within a pool."
+ def __init__(self, pool, message):
+ self.pool = pool
+ HTTPError.__init__(self, "%s: %s" % (pool, message))
+
+ def __reduce__(self):
+ # For pickling purposes.
+ return self.__class__, (None, None)
+
+
+class RequestError(PoolError):
+ "Base exception for PoolErrors that have associated URLs."
+ def __init__(self, pool, url, message):
+ self.url = url
+ PoolError.__init__(self, pool, message)
+
+ def __reduce__(self):
+ # For pickling purposes.
+ return self.__class__, (None, self.url, None)
+
+
+class SSLError(HTTPError):
+ "Raised when SSL certificate fails in an HTTPS connection."
+ pass
+
+
+class ProxyError(HTTPError):
+ "Raised when the connection to a proxy fails."
+ pass
+
+
+class DecodeError(HTTPError):
+ "Raised when automatic decoding based on Content-Type fails."
+ pass
+
+
+class ProtocolError(HTTPError):
+ "Raised when something unexpected happens mid-request/response."
+ pass
+
+
+#: Renamed to ProtocolError but aliased for backwards compatibility.
+ConnectionError = ProtocolError
+
+
+# Leaf Exceptions
+
+class MaxRetryError(RequestError):
+ """Raised when the maximum number of retries is exceeded.
+
+ :param pool: The connection pool
+ :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool`
+ :param string url: The requested Url
+ :param exceptions.Exception reason: The underlying error
+
+ """
+
+ def __init__(self, pool, url, reason=None):
+ self.reason = reason
+
+ message = "Max retries exceeded with url: %s (Caused by %r)" % (
+ url, reason)
+
+ RequestError.__init__(self, pool, url, message)
+
+
+class HostChangedError(RequestError):
+ "Raised when an existing pool gets a request for a foreign host."
+
+ def __init__(self, pool, url, retries=3):
+ message = "Tried to open a foreign host with url: %s" % url
+ RequestError.__init__(self, pool, url, message)
+ self.retries = retries
+
+
+class TimeoutStateError(HTTPError):
+ """ Raised when passing an invalid state to a timeout """
+ pass
+
+
+class TimeoutError(HTTPError):
+ """ Raised when a socket timeout error occurs.
+
+ Catching this error will catch both :exc:`ReadTimeoutErrors
+ ` and :exc:`ConnectTimeoutErrors `.
+ """
+ pass
+
+
+class ReadTimeoutError(TimeoutError, RequestError):
+ "Raised when a socket timeout occurs while receiving data from a server"
+ pass
+
+
+# This timeout error does not have a URL attached and needs to inherit from the
+# base HTTPError
+class ConnectTimeoutError(TimeoutError):
+ "Raised when a socket timeout occurs while connecting to a server"
+ pass
+
+
+class NewConnectionError(ConnectTimeoutError, PoolError):
+ "Raised when we fail to establish a new connection. Usually ECONNREFUSED."
+ pass
+
+
+class EmptyPoolError(PoolError):
+ "Raised when a pool runs out of connections and no more are allowed."
+ pass
+
+
+class ClosedPoolError(PoolError):
+ "Raised when a request enters a pool after the pool has been closed."
+ pass
+
+
+class LocationValueError(ValueError, HTTPError):
+ "Raised when there is something wrong with a given URL input."
+ pass
+
+
+class LocationParseError(LocationValueError):
+ "Raised when get_host or similar fails to parse the URL input."
+
+ def __init__(self, location):
+ message = "Failed to parse: %s" % location
+ HTTPError.__init__(self, message)
+
+ self.location = location
+
+
+class ResponseError(HTTPError):
+ "Used as a container for an error reason supplied in a MaxRetryError."
+ GENERIC_ERROR = 'too many error responses'
+ SPECIFIC_ERROR = 'too many {status_code} error responses'
+
+
+class SecurityWarning(HTTPWarning):
+ "Warned when perfoming security reducing actions"
+ pass
+
+
+class SubjectAltNameWarning(SecurityWarning):
+ "Warned when connecting to a host with a certificate missing a SAN."
+ pass
+
+
+class InsecureRequestWarning(SecurityWarning):
+ "Warned when making an unverified HTTPS request."
+ pass
+
+
+class SystemTimeWarning(SecurityWarning):
+ "Warned when system time is suspected to be wrong"
+ pass
+
+
+class InsecurePlatformWarning(SecurityWarning):
+ "Warned when certain SSL configuration is not available on a platform."
+ pass
+
+
+class SNIMissingWarning(HTTPWarning):
+ "Warned when making a HTTPS request without SNI available."
+ pass
+
+
+class DependencyWarning(HTTPWarning):
+ """
+ Warned when an attempt is made to import a module with missing optional
+ dependencies.
+ """
+ pass
+
+
+class ResponseNotChunked(ProtocolError, ValueError):
+ "Response needs to be chunked in order to read it as chunks."
+ pass
+
+
+class BodyNotHttplibCompatible(HTTPError):
+ """
+ Body should be httplib.HTTPResponse like (have an fp attribute which
+ returns raw chunks) for read_chunked().
+ """
+ pass
+
+
+class IncompleteRead(HTTPError, httplib_IncompleteRead):
+ """
+ Response length doesn't match expected Content-Length
+
+ Subclass of http_client.IncompleteRead to allow int value
+ for `partial` to avoid creating large objects on streamed
+ reads.
+ """
+ def __init__(self, partial, expected):
+ super(IncompleteRead, self).__init__(partial, expected)
+
+ def __repr__(self):
+ return ('IncompleteRead(%i bytes read, '
+ '%i more expected)' % (self.partial, self.expected))
+
+
+class InvalidHeader(HTTPError):
+ "The header provided was somehow invalid."
+ pass
+
+
+class ProxySchemeUnknown(AssertionError, ValueError):
+ "ProxyManager does not support the supplied scheme"
+ # TODO(t-8ch): Stop inheriting from AssertionError in v2.0.
+
+ def __init__(self, scheme):
+ message = "Not supported proxy scheme %s" % scheme
+ super(ProxySchemeUnknown, self).__init__(message)
+
+
+class HeaderParsingError(HTTPError):
+ "Raised by assert_header_parsing, but we convert it to a log.warning statement."
+ def __init__(self, defects, unparsed_data):
+ message = '%s, unparsed data: %r' % (defects or 'Unknown', unparsed_data)
+ super(HeaderParsingError, self).__init__(message)
+
+
+class UnrewindableBodyError(HTTPError):
+ "urllib3 encountered an error when trying to rewind a body"
+ pass
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/fields.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/fields.py
index 8e15621..19b0ae0 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/fields.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/fields.py
@@ -1,178 +1,178 @@
-from __future__ import absolute_import
-import email.utils
-import mimetypes
-
-from .packages import six
-
-
-def guess_content_type(filename, default='application/octet-stream'):
- """
- Guess the "Content-Type" of a file.
-
- :param filename:
- The filename to guess the "Content-Type" of using :mod:`mimetypes`.
- :param default:
- If no "Content-Type" can be guessed, default to `default`.
- """
- if filename:
- return mimetypes.guess_type(filename)[0] or default
- return default
-
-
-def format_header_param(name, value):
- """
- Helper function to format and quote a single header parameter.
-
- Particularly useful for header parameters which might contain
- non-ASCII values, like file names. This follows RFC 2231, as
- suggested by RFC 2388 Section 4.4.
-
- :param name:
- The name of the parameter, a string expected to be ASCII only.
- :param value:
- The value of the parameter, provided as a unicode string.
- """
- if not any(ch in value for ch in '"\\\r\n'):
- result = '%s="%s"' % (name, value)
- try:
- result.encode('ascii')
- except (UnicodeEncodeError, UnicodeDecodeError):
- pass
- else:
- return result
- if not six.PY3 and isinstance(value, six.text_type): # Python 2:
- value = value.encode('utf-8')
- value = email.utils.encode_rfc2231(value, 'utf-8')
- value = '%s*=%s' % (name, value)
- return value
-
-
-class RequestField(object):
- """
- A data container for request body parameters.
-
- :param name:
- The name of this request field.
- :param data:
- The data/value body.
- :param filename:
- An optional filename of the request field.
- :param headers:
- An optional dict-like object of headers to initially use for the field.
- """
- def __init__(self, name, data, filename=None, headers=None):
- self._name = name
- self._filename = filename
- self.data = data
- self.headers = {}
- if headers:
- self.headers = dict(headers)
-
- @classmethod
- def from_tuples(cls, fieldname, value):
- """
- A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters.
-
- Supports constructing :class:`~urllib3.fields.RequestField` from
- parameter of key/value strings AND key/filetuple. A filetuple is a
- (filename, data, MIME type) tuple where the MIME type is optional.
- For example::
-
- 'foo': 'bar',
- 'fakefile': ('foofile.txt', 'contents of foofile'),
- 'realfile': ('barfile.txt', open('realfile').read()),
- 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'),
- 'nonamefile': 'contents of nonamefile field',
-
- Field names and filenames must be unicode.
- """
- if isinstance(value, tuple):
- if len(value) == 3:
- filename, data, content_type = value
- else:
- filename, data = value
- content_type = guess_content_type(filename)
- else:
- filename = None
- content_type = None
- data = value
-
- request_param = cls(fieldname, data, filename=filename)
- request_param.make_multipart(content_type=content_type)
-
- return request_param
-
- def _render_part(self, name, value):
- """
- Overridable helper function to format a single header parameter.
-
- :param name:
- The name of the parameter, a string expected to be ASCII only.
- :param value:
- The value of the parameter, provided as a unicode string.
- """
- return format_header_param(name, value)
-
- def _render_parts(self, header_parts):
- """
- Helper function to format and quote a single header.
-
- Useful for single headers that are composed of multiple items. E.g.,
- 'Content-Disposition' fields.
-
- :param header_parts:
- A sequence of (k, v) typles or a :class:`dict` of (k, v) to format
- as `k1="v1"; k2="v2"; ...`.
- """
- parts = []
- iterable = header_parts
- if isinstance(header_parts, dict):
- iterable = header_parts.items()
-
- for name, value in iterable:
- if value is not None:
- parts.append(self._render_part(name, value))
-
- return '; '.join(parts)
-
- def render_headers(self):
- """
- Renders the headers for this request field.
- """
- lines = []
-
- sort_keys = ['Content-Disposition', 'Content-Type', 'Content-Location']
- for sort_key in sort_keys:
- if self.headers.get(sort_key, False):
- lines.append('%s: %s' % (sort_key, self.headers[sort_key]))
-
- for header_name, header_value in self.headers.items():
- if header_name not in sort_keys:
- if header_value:
- lines.append('%s: %s' % (header_name, header_value))
-
- lines.append('\r\n')
- return '\r\n'.join(lines)
-
- def make_multipart(self, content_disposition=None, content_type=None,
- content_location=None):
- """
- Makes this request field into a multipart request field.
-
- This method overrides "Content-Disposition", "Content-Type" and
- "Content-Location" headers to the request parameter.
-
- :param content_type:
- The 'Content-Type' of the request body.
- :param content_location:
- The 'Content-Location' of the request body.
-
- """
- self.headers['Content-Disposition'] = content_disposition or 'form-data'
- self.headers['Content-Disposition'] += '; '.join([
- '', self._render_parts(
- (('name', self._name), ('filename', self._filename))
- )
- ])
- self.headers['Content-Type'] = content_type
- self.headers['Content-Location'] = content_location
+from __future__ import absolute_import
+import email.utils
+import mimetypes
+
+from .packages import six
+
+
+def guess_content_type(filename, default='application/octet-stream'):
+ """
+ Guess the "Content-Type" of a file.
+
+ :param filename:
+ The filename to guess the "Content-Type" of using :mod:`mimetypes`.
+ :param default:
+ If no "Content-Type" can be guessed, default to `default`.
+ """
+ if filename:
+ return mimetypes.guess_type(filename)[0] or default
+ return default
+
+
+def format_header_param(name, value):
+ """
+ Helper function to format and quote a single header parameter.
+
+ Particularly useful for header parameters which might contain
+ non-ASCII values, like file names. This follows RFC 2231, as
+ suggested by RFC 2388 Section 4.4.
+
+ :param name:
+ The name of the parameter, a string expected to be ASCII only.
+ :param value:
+ The value of the parameter, provided as a unicode string.
+ """
+ if not any(ch in value for ch in '"\\\r\n'):
+ result = '%s="%s"' % (name, value)
+ try:
+ result.encode('ascii')
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ pass
+ else:
+ return result
+ if not six.PY3 and isinstance(value, six.text_type): # Python 2:
+ value = value.encode('utf-8')
+ value = email.utils.encode_rfc2231(value, 'utf-8')
+ value = '%s*=%s' % (name, value)
+ return value
+
+
+class RequestField(object):
+ """
+ A data container for request body parameters.
+
+ :param name:
+ The name of this request field.
+ :param data:
+ The data/value body.
+ :param filename:
+ An optional filename of the request field.
+ :param headers:
+ An optional dict-like object of headers to initially use for the field.
+ """
+ def __init__(self, name, data, filename=None, headers=None):
+ self._name = name
+ self._filename = filename
+ self.data = data
+ self.headers = {}
+ if headers:
+ self.headers = dict(headers)
+
+ @classmethod
+ def from_tuples(cls, fieldname, value):
+ """
+ A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters.
+
+ Supports constructing :class:`~urllib3.fields.RequestField` from
+ parameter of key/value strings AND key/filetuple. A filetuple is a
+ (filename, data, MIME type) tuple where the MIME type is optional.
+ For example::
+
+ 'foo': 'bar',
+ 'fakefile': ('foofile.txt', 'contents of foofile'),
+ 'realfile': ('barfile.txt', open('realfile').read()),
+ 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'),
+ 'nonamefile': 'contents of nonamefile field',
+
+ Field names and filenames must be unicode.
+ """
+ if isinstance(value, tuple):
+ if len(value) == 3:
+ filename, data, content_type = value
+ else:
+ filename, data = value
+ content_type = guess_content_type(filename)
+ else:
+ filename = None
+ content_type = None
+ data = value
+
+ request_param = cls(fieldname, data, filename=filename)
+ request_param.make_multipart(content_type=content_type)
+
+ return request_param
+
+ def _render_part(self, name, value):
+ """
+ Overridable helper function to format a single header parameter.
+
+ :param name:
+ The name of the parameter, a string expected to be ASCII only.
+ :param value:
+ The value of the parameter, provided as a unicode string.
+ """
+ return format_header_param(name, value)
+
+ def _render_parts(self, header_parts):
+ """
+ Helper function to format and quote a single header.
+
+ Useful for single headers that are composed of multiple items. E.g.,
+ 'Content-Disposition' fields.
+
+ :param header_parts:
+ A sequence of (k, v) typles or a :class:`dict` of (k, v) to format
+ as `k1="v1"; k2="v2"; ...`.
+ """
+ parts = []
+ iterable = header_parts
+ if isinstance(header_parts, dict):
+ iterable = header_parts.items()
+
+ for name, value in iterable:
+ if value is not None:
+ parts.append(self._render_part(name, value))
+
+ return '; '.join(parts)
+
+ def render_headers(self):
+ """
+ Renders the headers for this request field.
+ """
+ lines = []
+
+ sort_keys = ['Content-Disposition', 'Content-Type', 'Content-Location']
+ for sort_key in sort_keys:
+ if self.headers.get(sort_key, False):
+ lines.append('%s: %s' % (sort_key, self.headers[sort_key]))
+
+ for header_name, header_value in self.headers.items():
+ if header_name not in sort_keys:
+ if header_value:
+ lines.append('%s: %s' % (header_name, header_value))
+
+ lines.append('\r\n')
+ return '\r\n'.join(lines)
+
+ def make_multipart(self, content_disposition=None, content_type=None,
+ content_location=None):
+ """
+ Makes this request field into a multipart request field.
+
+ This method overrides "Content-Disposition", "Content-Type" and
+ "Content-Location" headers to the request parameter.
+
+ :param content_type:
+ The 'Content-Type' of the request body.
+ :param content_location:
+ The 'Content-Location' of the request body.
+
+ """
+ self.headers['Content-Disposition'] = content_disposition or 'form-data'
+ self.headers['Content-Disposition'] += '; '.join([
+ '', self._render_parts(
+ (('name', self._name), ('filename', self._filename))
+ )
+ ])
+ self.headers['Content-Type'] = content_type
+ self.headers['Content-Location'] = content_location
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/filepost.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/filepost.py
index e53dedc..cd11cee 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/filepost.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/filepost.py
@@ -1,94 +1,94 @@
-from __future__ import absolute_import
-import codecs
-
-from uuid import uuid4
-from io import BytesIO
-
-from .packages import six
-from .packages.six import b
-from .fields import RequestField
-
-writer = codecs.lookup('utf-8')[3]
-
-
-def choose_boundary():
- """
- Our embarrassingly-simple replacement for mimetools.choose_boundary.
- """
- return uuid4().hex
-
-
-def iter_field_objects(fields):
- """
- Iterate over fields.
-
- Supports list of (k, v) tuples and dicts, and lists of
- :class:`~urllib3.fields.RequestField`.
-
- """
- if isinstance(fields, dict):
- i = six.iteritems(fields)
- else:
- i = iter(fields)
-
- for field in i:
- if isinstance(field, RequestField):
- yield field
- else:
- yield RequestField.from_tuples(*field)
-
-
-def iter_fields(fields):
- """
- .. deprecated:: 1.6
-
- Iterate over fields.
-
- The addition of :class:`~urllib3.fields.RequestField` makes this function
- obsolete. Instead, use :func:`iter_field_objects`, which returns
- :class:`~urllib3.fields.RequestField` objects.
-
- Supports list of (k, v) tuples and dicts.
- """
- if isinstance(fields, dict):
- return ((k, v) for k, v in six.iteritems(fields))
-
- return ((k, v) for k, v in fields)
-
-
-def encode_multipart_formdata(fields, boundary=None):
- """
- Encode a dictionary of ``fields`` using the multipart/form-data MIME format.
-
- :param fields:
- Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`).
-
- :param boundary:
- If not specified, then a random boundary will be generated using
- :func:`mimetools.choose_boundary`.
- """
- body = BytesIO()
- if boundary is None:
- boundary = choose_boundary()
-
- for field in iter_field_objects(fields):
- body.write(b('--%s\r\n' % (boundary)))
-
- writer(body).write(field.render_headers())
- data = field.data
-
- if isinstance(data, int):
- data = str(data) # Backwards compatibility
-
- if isinstance(data, six.text_type):
- writer(body).write(data)
- else:
- body.write(data)
-
- body.write(b'\r\n')
-
- body.write(b('--%s--\r\n' % (boundary)))
-
- content_type = str('multipart/form-data; boundary=%s' % boundary)
-
- return body.getvalue(), content_type
+from __future__ import absolute_import
+import codecs
+
+from uuid import uuid4
+from io import BytesIO
+
+from .packages import six
+from .packages.six import b
+from .fields import RequestField
+
+writer = codecs.lookup('utf-8')[3]
+
+
+def choose_boundary():
+ """
+ Our embarrassingly-simple replacement for mimetools.choose_boundary.
+ """
+ return uuid4().hex
+
+
+def iter_field_objects(fields):
+ """
+ Iterate over fields.
+
+ Supports list of (k, v) tuples and dicts, and lists of
+ :class:`~urllib3.fields.RequestField`.
+
+ """
+ if isinstance(fields, dict):
+ i = six.iteritems(fields)
+ else:
+ i = iter(fields)
+
+ for field in i:
+ if isinstance(field, RequestField):
+ yield field
+ else:
+ yield RequestField.from_tuples(*field)
+
+
+def iter_fields(fields):
+ """
+ .. deprecated:: 1.6
+
+ Iterate over fields.
+
+ The addition of :class:`~urllib3.fields.RequestField` makes this function
+ obsolete. Instead, use :func:`iter_field_objects`, which returns
+ :class:`~urllib3.fields.RequestField` objects.
+
+ Supports list of (k, v) tuples and dicts.
+ """
+ if isinstance(fields, dict):
+ return ((k, v) for k, v in six.iteritems(fields))
+
+ return ((k, v) for k, v in fields)
+
+
+def encode_multipart_formdata(fields, boundary=None):
+ """
+ Encode a dictionary of ``fields`` using the multipart/form-data MIME format.
+
+ :param fields:
+ Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`).
+
+ :param boundary:
+ If not specified, then a random boundary will be generated using
+ :func:`mimetools.choose_boundary`.
+ """
+ body = BytesIO()
+ if boundary is None:
+ boundary = choose_boundary()
+
+ for field in iter_field_objects(fields):
+ body.write(b('--%s\r\n' % (boundary)))
+
+ writer(body).write(field.render_headers())
+ data = field.data
+
+ if isinstance(data, int):
+ data = str(data) # Backwards compatibility
+
+ if isinstance(data, six.text_type):
+ writer(body).write(data)
+ else:
+ body.write(data)
+
+ body.write(b'\r\n')
+
+ body.write(b('--%s--\r\n' % (boundary)))
+
+ content_type = str('multipart/form-data; boundary=%s' % boundary)
+
+ return body.getvalue(), content_type
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/__init__.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/__init__.py
index 324c551..170e974 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/__init__.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/__init__.py
@@ -1,5 +1,5 @@
-from __future__ import absolute_import
-
-from . import ssl_match_hostname
-
-__all__ = ('ssl_match_hostname', )
+from __future__ import absolute_import
+
+from . import ssl_match_hostname
+
+__all__ = ('ssl_match_hostname', )
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/backports/makefile.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/backports/makefile.py
index 00dee0b..75b80dc 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/backports/makefile.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/backports/makefile.py
@@ -1,53 +1,53 @@
-# -*- coding: utf-8 -*-
-"""
-backports.makefile
-~~~~~~~~~~~~~~~~~~
-
-Backports the Python 3 ``socket.makefile`` method for use with anything that
-wants to create a "fake" socket object.
-"""
-import io
-
-from socket import SocketIO
-
-
-def backport_makefile(self, mode="r", buffering=None, encoding=None,
- errors=None, newline=None):
- """
- Backport of ``socket.makefile`` from Python 3.5.
- """
- if not set(mode) <= set(["r", "w", "b"]):
- raise ValueError(
- "invalid mode %r (only r, w, b allowed)" % (mode,)
- )
- writing = "w" in mode
- reading = "r" in mode or not writing
- assert reading or writing
- binary = "b" in mode
- rawmode = ""
- if reading:
- rawmode += "r"
- if writing:
- rawmode += "w"
- raw = SocketIO(self, rawmode)
- self._makefile_refs += 1
- if buffering is None:
- buffering = -1
- if buffering < 0:
- buffering = io.DEFAULT_BUFFER_SIZE
- if buffering == 0:
- if not binary:
- raise ValueError("unbuffered streams must be binary")
- return raw
- if reading and writing:
- buffer = io.BufferedRWPair(raw, raw, buffering)
- elif reading:
- buffer = io.BufferedReader(raw, buffering)
- else:
- assert writing
- buffer = io.BufferedWriter(raw, buffering)
- if binary:
- return buffer
- text = io.TextIOWrapper(buffer, encoding, errors, newline)
- text.mode = mode
- return text
+# -*- coding: utf-8 -*-
+"""
+backports.makefile
+~~~~~~~~~~~~~~~~~~
+
+Backports the Python 3 ``socket.makefile`` method for use with anything that
+wants to create a "fake" socket object.
+"""
+import io
+
+from socket import SocketIO
+
+
+def backport_makefile(self, mode="r", buffering=None, encoding=None,
+ errors=None, newline=None):
+ """
+ Backport of ``socket.makefile`` from Python 3.5.
+ """
+ if not set(mode) <= set(["r", "w", "b"]):
+ raise ValueError(
+ "invalid mode %r (only r, w, b allowed)" % (mode,)
+ )
+ writing = "w" in mode
+ reading = "r" in mode or not writing
+ assert reading or writing
+ binary = "b" in mode
+ rawmode = ""
+ if reading:
+ rawmode += "r"
+ if writing:
+ rawmode += "w"
+ raw = SocketIO(self, rawmode)
+ self._makefile_refs += 1
+ if buffering is None:
+ buffering = -1
+ if buffering < 0:
+ buffering = io.DEFAULT_BUFFER_SIZE
+ if buffering == 0:
+ if not binary:
+ raise ValueError("unbuffered streams must be binary")
+ return raw
+ if reading and writing:
+ buffer = io.BufferedRWPair(raw, raw, buffering)
+ elif reading:
+ buffer = io.BufferedReader(raw, buffering)
+ else:
+ assert writing
+ buffer = io.BufferedWriter(raw, buffering)
+ if binary:
+ return buffer
+ text = io.TextIOWrapper(buffer, encoding, errors, newline)
+ text.mode = mode
+ return text
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ordered_dict.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ordered_dict.py
index 62dcb42..4479363 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ordered_dict.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ordered_dict.py
@@ -1,259 +1,259 @@
-# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
-# Passes Python2.7's test suite and incorporates all the latest updates.
-# Copyright 2009 Raymond Hettinger, released under the MIT License.
-# http://code.activestate.com/recipes/576693/
-try:
- from thread import get_ident as _get_ident
-except ImportError:
- from dummy_thread import get_ident as _get_ident
-
-try:
- from _abcoll import KeysView, ValuesView, ItemsView
-except ImportError:
- pass
-
-
-class OrderedDict(dict):
- 'Dictionary that remembers insertion order'
- # An inherited dict maps keys to values.
- # The inherited dict provides __getitem__, __len__, __contains__, and get.
- # The remaining methods are order-aware.
- # Big-O running times for all methods are the same as for regular dictionaries.
-
- # The internal self.__map dictionary maps keys to links in a doubly linked list.
- # The circular doubly linked list starts and ends with a sentinel element.
- # The sentinel element never gets deleted (this simplifies the algorithm).
- # Each link is stored as a list of length three: [PREV, NEXT, KEY].
-
- def __init__(self, *args, **kwds):
- '''Initialize an ordered dictionary. Signature is the same as for
- regular dictionaries, but keyword arguments are not recommended
- because their insertion order is arbitrary.
-
- '''
- if len(args) > 1:
- raise TypeError('expected at most 1 arguments, got %d' % len(args))
- try:
- self.__root
- except AttributeError:
- self.__root = root = [] # sentinel node
- root[:] = [root, root, None]
- self.__map = {}
- self.__update(*args, **kwds)
-
- def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
- 'od.__setitem__(i, y) <==> od[i]=y'
- # Setting a new item creates a new link which goes at the end of the linked
- # list, and the inherited dictionary is updated with the new key/value pair.
- if key not in self:
- root = self.__root
- last = root[0]
- last[1] = root[0] = self.__map[key] = [last, root, key]
- dict_setitem(self, key, value)
-
- def __delitem__(self, key, dict_delitem=dict.__delitem__):
- 'od.__delitem__(y) <==> del od[y]'
- # Deleting an existing item uses self.__map to find the link which is
- # then removed by updating the links in the predecessor and successor nodes.
- dict_delitem(self, key)
- link_prev, link_next, key = self.__map.pop(key)
- link_prev[1] = link_next
- link_next[0] = link_prev
-
- def __iter__(self):
- 'od.__iter__() <==> iter(od)'
- root = self.__root
- curr = root[1]
- while curr is not root:
- yield curr[2]
- curr = curr[1]
-
- def __reversed__(self):
- 'od.__reversed__() <==> reversed(od)'
- root = self.__root
- curr = root[0]
- while curr is not root:
- yield curr[2]
- curr = curr[0]
-
- def clear(self):
- 'od.clear() -> None. Remove all items from od.'
- try:
- for node in self.__map.itervalues():
- del node[:]
- root = self.__root
- root[:] = [root, root, None]
- self.__map.clear()
- except AttributeError:
- pass
- dict.clear(self)
-
- def popitem(self, last=True):
- '''od.popitem() -> (k, v), return and remove a (key, value) pair.
- Pairs are returned in LIFO order if last is true or FIFO order if false.
-
- '''
- if not self:
- raise KeyError('dictionary is empty')
- root = self.__root
- if last:
- link = root[0]
- link_prev = link[0]
- link_prev[1] = root
- root[0] = link_prev
- else:
- link = root[1]
- link_next = link[1]
- root[1] = link_next
- link_next[0] = root
- key = link[2]
- del self.__map[key]
- value = dict.pop(self, key)
- return key, value
-
- # -- the following methods do not depend on the internal structure --
-
- def keys(self):
- 'od.keys() -> list of keys in od'
- return list(self)
-
- def values(self):
- 'od.values() -> list of values in od'
- return [self[key] for key in self]
-
- def items(self):
- 'od.items() -> list of (key, value) pairs in od'
- return [(key, self[key]) for key in self]
-
- def iterkeys(self):
- 'od.iterkeys() -> an iterator over the keys in od'
- return iter(self)
-
- def itervalues(self):
- 'od.itervalues -> an iterator over the values in od'
- for k in self:
- yield self[k]
-
- def iteritems(self):
- 'od.iteritems -> an iterator over the (key, value) items in od'
- for k in self:
- yield (k, self[k])
-
- def update(*args, **kwds):
- '''od.update(E, **F) -> None. Update od from dict/iterable E and F.
-
- If E is a dict instance, does: for k in E: od[k] = E[k]
- If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
- Or if E is an iterable of items, does: for k, v in E: od[k] = v
- In either case, this is followed by: for k, v in F.items(): od[k] = v
-
- '''
- if len(args) > 2:
- raise TypeError('update() takes at most 2 positional '
- 'arguments (%d given)' % (len(args),))
- elif not args:
- raise TypeError('update() takes at least 1 argument (0 given)')
- self = args[0]
- # Make progressively weaker assumptions about "other"
- other = ()
- if len(args) == 2:
- other = args[1]
- if isinstance(other, dict):
- for key in other:
- self[key] = other[key]
- elif hasattr(other, 'keys'):
- for key in other.keys():
- self[key] = other[key]
- else:
- for key, value in other:
- self[key] = value
- for key, value in kwds.items():
- self[key] = value
-
- __update = update # let subclasses override update without breaking __init__
-
- __marker = object()
-
- def pop(self, key, default=__marker):
- '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
- If key is not found, d is returned if given, otherwise KeyError is raised.
-
- '''
- if key in self:
- result = self[key]
- del self[key]
- return result
- if default is self.__marker:
- raise KeyError(key)
- return default
-
- def setdefault(self, key, default=None):
- 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
- if key in self:
- return self[key]
- self[key] = default
- return default
-
- def __repr__(self, _repr_running={}):
- 'od.__repr__() <==> repr(od)'
- call_key = id(self), _get_ident()
- if call_key in _repr_running:
- return '...'
- _repr_running[call_key] = 1
- try:
- if not self:
- return '%s()' % (self.__class__.__name__,)
- return '%s(%r)' % (self.__class__.__name__, self.items())
- finally:
- del _repr_running[call_key]
-
- def __reduce__(self):
- 'Return state information for pickling'
- items = [[k, self[k]] for k in self]
- inst_dict = vars(self).copy()
- for k in vars(OrderedDict()):
- inst_dict.pop(k, None)
- if inst_dict:
- return (self.__class__, (items,), inst_dict)
- return self.__class__, (items,)
-
- def copy(self):
- 'od.copy() -> a shallow copy of od'
- return self.__class__(self)
-
- @classmethod
- def fromkeys(cls, iterable, value=None):
- '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
- and values equal to v (which defaults to None).
-
- '''
- d = cls()
- for key in iterable:
- d[key] = value
- return d
-
- def __eq__(self, other):
- '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
- while comparison to a regular mapping is order-insensitive.
-
- '''
- if isinstance(other, OrderedDict):
- return len(self)==len(other) and self.items() == other.items()
- return dict.__eq__(self, other)
-
- def __ne__(self, other):
- return not self == other
-
- # -- the following methods are only used in Python 2.7 --
-
- def viewkeys(self):
- "od.viewkeys() -> a set-like object providing a view on od's keys"
- return KeysView(self)
-
- def viewvalues(self):
- "od.viewvalues() -> an object providing a view on od's values"
- return ValuesView(self)
-
- def viewitems(self):
- "od.viewitems() -> a set-like object providing a view on od's items"
- return ItemsView(self)
+# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
+# Passes Python2.7's test suite and incorporates all the latest updates.
+# Copyright 2009 Raymond Hettinger, released under the MIT License.
+# http://code.activestate.com/recipes/576693/
+try:
+ from thread import get_ident as _get_ident
+except ImportError:
+ from dummy_thread import get_ident as _get_ident
+
+try:
+ from _abcoll import KeysView, ValuesView, ItemsView
+except ImportError:
+ pass
+
+
+class OrderedDict(dict):
+ 'Dictionary that remembers insertion order'
+ # An inherited dict maps keys to values.
+ # The inherited dict provides __getitem__, __len__, __contains__, and get.
+ # The remaining methods are order-aware.
+ # Big-O running times for all methods are the same as for regular dictionaries.
+
+ # The internal self.__map dictionary maps keys to links in a doubly linked list.
+ # The circular doubly linked list starts and ends with a sentinel element.
+ # The sentinel element never gets deleted (this simplifies the algorithm).
+ # Each link is stored as a list of length three: [PREV, NEXT, KEY].
+
+ def __init__(self, *args, **kwds):
+ '''Initialize an ordered dictionary. Signature is the same as for
+ regular dictionaries, but keyword arguments are not recommended
+ because their insertion order is arbitrary.
+
+ '''
+ if len(args) > 1:
+ raise TypeError('expected at most 1 arguments, got %d' % len(args))
+ try:
+ self.__root
+ except AttributeError:
+ self.__root = root = [] # sentinel node
+ root[:] = [root, root, None]
+ self.__map = {}
+ self.__update(*args, **kwds)
+
+ def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
+ 'od.__setitem__(i, y) <==> od[i]=y'
+ # Setting a new item creates a new link which goes at the end of the linked
+ # list, and the inherited dictionary is updated with the new key/value pair.
+ if key not in self:
+ root = self.__root
+ last = root[0]
+ last[1] = root[0] = self.__map[key] = [last, root, key]
+ dict_setitem(self, key, value)
+
+ def __delitem__(self, key, dict_delitem=dict.__delitem__):
+ 'od.__delitem__(y) <==> del od[y]'
+ # Deleting an existing item uses self.__map to find the link which is
+ # then removed by updating the links in the predecessor and successor nodes.
+ dict_delitem(self, key)
+ link_prev, link_next, key = self.__map.pop(key)
+ link_prev[1] = link_next
+ link_next[0] = link_prev
+
+ def __iter__(self):
+ 'od.__iter__() <==> iter(od)'
+ root = self.__root
+ curr = root[1]
+ while curr is not root:
+ yield curr[2]
+ curr = curr[1]
+
+ def __reversed__(self):
+ 'od.__reversed__() <==> reversed(od)'
+ root = self.__root
+ curr = root[0]
+ while curr is not root:
+ yield curr[2]
+ curr = curr[0]
+
+ def clear(self):
+ 'od.clear() -> None. Remove all items from od.'
+ try:
+ for node in self.__map.itervalues():
+ del node[:]
+ root = self.__root
+ root[:] = [root, root, None]
+ self.__map.clear()
+ except AttributeError:
+ pass
+ dict.clear(self)
+
+ def popitem(self, last=True):
+ '''od.popitem() -> (k, v), return and remove a (key, value) pair.
+ Pairs are returned in LIFO order if last is true or FIFO order if false.
+
+ '''
+ if not self:
+ raise KeyError('dictionary is empty')
+ root = self.__root
+ if last:
+ link = root[0]
+ link_prev = link[0]
+ link_prev[1] = root
+ root[0] = link_prev
+ else:
+ link = root[1]
+ link_next = link[1]
+ root[1] = link_next
+ link_next[0] = root
+ key = link[2]
+ del self.__map[key]
+ value = dict.pop(self, key)
+ return key, value
+
+ # -- the following methods do not depend on the internal structure --
+
+ def keys(self):
+ 'od.keys() -> list of keys in od'
+ return list(self)
+
+ def values(self):
+ 'od.values() -> list of values in od'
+ return [self[key] for key in self]
+
+ def items(self):
+ 'od.items() -> list of (key, value) pairs in od'
+ return [(key, self[key]) for key in self]
+
+ def iterkeys(self):
+ 'od.iterkeys() -> an iterator over the keys in od'
+ return iter(self)
+
+ def itervalues(self):
+ 'od.itervalues -> an iterator over the values in od'
+ for k in self:
+ yield self[k]
+
+ def iteritems(self):
+ 'od.iteritems -> an iterator over the (key, value) items in od'
+ for k in self:
+ yield (k, self[k])
+
+ def update(*args, **kwds):
+ '''od.update(E, **F) -> None. Update od from dict/iterable E and F.
+
+ If E is a dict instance, does: for k in E: od[k] = E[k]
+ If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
+ Or if E is an iterable of items, does: for k, v in E: od[k] = v
+ In either case, this is followed by: for k, v in F.items(): od[k] = v
+
+ '''
+ if len(args) > 2:
+ raise TypeError('update() takes at most 2 positional '
+ 'arguments (%d given)' % (len(args),))
+ elif not args:
+ raise TypeError('update() takes at least 1 argument (0 given)')
+ self = args[0]
+ # Make progressively weaker assumptions about "other"
+ other = ()
+ if len(args) == 2:
+ other = args[1]
+ if isinstance(other, dict):
+ for key in other:
+ self[key] = other[key]
+ elif hasattr(other, 'keys'):
+ for key in other.keys():
+ self[key] = other[key]
+ else:
+ for key, value in other:
+ self[key] = value
+ for key, value in kwds.items():
+ self[key] = value
+
+ __update = update # let subclasses override update without breaking __init__
+
+ __marker = object()
+
+ def pop(self, key, default=__marker):
+ '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
+ If key is not found, d is returned if given, otherwise KeyError is raised.
+
+ '''
+ if key in self:
+ result = self[key]
+ del self[key]
+ return result
+ if default is self.__marker:
+ raise KeyError(key)
+ return default
+
+ def setdefault(self, key, default=None):
+ 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
+ if key in self:
+ return self[key]
+ self[key] = default
+ return default
+
+ def __repr__(self, _repr_running={}):
+ 'od.__repr__() <==> repr(od)'
+ call_key = id(self), _get_ident()
+ if call_key in _repr_running:
+ return '...'
+ _repr_running[call_key] = 1
+ try:
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, self.items())
+ finally:
+ del _repr_running[call_key]
+
+ def __reduce__(self):
+ 'Return state information for pickling'
+ items = [[k, self[k]] for k in self]
+ inst_dict = vars(self).copy()
+ for k in vars(OrderedDict()):
+ inst_dict.pop(k, None)
+ if inst_dict:
+ return (self.__class__, (items,), inst_dict)
+ return self.__class__, (items,)
+
+ def copy(self):
+ 'od.copy() -> a shallow copy of od'
+ return self.__class__(self)
+
+ @classmethod
+ def fromkeys(cls, iterable, value=None):
+ '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
+ and values equal to v (which defaults to None).
+
+ '''
+ d = cls()
+ for key in iterable:
+ d[key] = value
+ return d
+
+ def __eq__(self, other):
+ '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
+ while comparison to a regular mapping is order-insensitive.
+
+ '''
+ if isinstance(other, OrderedDict):
+ return len(self)==len(other) and self.items() == other.items()
+ return dict.__eq__(self, other)
+
+ def __ne__(self, other):
+ return not self == other
+
+ # -- the following methods are only used in Python 2.7 --
+
+ def viewkeys(self):
+ "od.viewkeys() -> a set-like object providing a view on od's keys"
+ return KeysView(self)
+
+ def viewvalues(self):
+ "od.viewvalues() -> an object providing a view on od's values"
+ return ValuesView(self)
+
+ def viewitems(self):
+ "od.viewitems() -> a set-like object providing a view on od's items"
+ return ItemsView(self)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/six.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/six.py
index 7bd9225..190c023 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/six.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/six.py
@@ -1,868 +1,868 @@
-"""Utilities for writing code that runs on Python 2 and 3"""
-
-# Copyright (c) 2010-2015 Benjamin Peterson
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-from __future__ import absolute_import
-
-import functools
-import itertools
-import operator
-import sys
-import types
-
-__author__ = "Benjamin Peterson "
-__version__ = "1.10.0"
-
-
-# Useful for very coarse version differentiation.
-PY2 = sys.version_info[0] == 2
-PY3 = sys.version_info[0] == 3
-PY34 = sys.version_info[0:2] >= (3, 4)
-
-if PY3:
- string_types = str,
- integer_types = int,
- class_types = type,
- text_type = str
- binary_type = bytes
-
- MAXSIZE = sys.maxsize
-else:
- string_types = basestring,
- integer_types = (int, long)
- class_types = (type, types.ClassType)
- text_type = unicode
- binary_type = str
-
- if sys.platform.startswith("java"):
- # Jython always uses 32 bits.
- MAXSIZE = int((1 << 31) - 1)
- else:
- # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
- class X(object):
-
- def __len__(self):
- return 1 << 31
- try:
- len(X())
- except OverflowError:
- # 32-bit
- MAXSIZE = int((1 << 31) - 1)
- else:
- # 64-bit
- MAXSIZE = int((1 << 63) - 1)
- del X
-
-
-def _add_doc(func, doc):
- """Add documentation to a function."""
- func.__doc__ = doc
-
-
-def _import_module(name):
- """Import module, returning the module after the last dot."""
- __import__(name)
- return sys.modules[name]
-
-
-class _LazyDescr(object):
-
- def __init__(self, name):
- self.name = name
-
- def __get__(self, obj, tp):
- result = self._resolve()
- setattr(obj, self.name, result) # Invokes __set__.
- try:
- # This is a bit ugly, but it avoids running this again by
- # removing this descriptor.
- delattr(obj.__class__, self.name)
- except AttributeError:
- pass
- return result
-
-
-class MovedModule(_LazyDescr):
-
- def __init__(self, name, old, new=None):
- super(MovedModule, self).__init__(name)
- if PY3:
- if new is None:
- new = name
- self.mod = new
- else:
- self.mod = old
-
- def _resolve(self):
- return _import_module(self.mod)
-
- def __getattr__(self, attr):
- _module = self._resolve()
- value = getattr(_module, attr)
- setattr(self, attr, value)
- return value
-
-
-class _LazyModule(types.ModuleType):
-
- def __init__(self, name):
- super(_LazyModule, self).__init__(name)
- self.__doc__ = self.__class__.__doc__
-
- def __dir__(self):
- attrs = ["__doc__", "__name__"]
- attrs += [attr.name for attr in self._moved_attributes]
- return attrs
-
- # Subclasses should override this
- _moved_attributes = []
-
-
-class MovedAttribute(_LazyDescr):
-
- def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
- super(MovedAttribute, self).__init__(name)
- if PY3:
- if new_mod is None:
- new_mod = name
- self.mod = new_mod
- if new_attr is None:
- if old_attr is None:
- new_attr = name
- else:
- new_attr = old_attr
- self.attr = new_attr
- else:
- self.mod = old_mod
- if old_attr is None:
- old_attr = name
- self.attr = old_attr
-
- def _resolve(self):
- module = _import_module(self.mod)
- return getattr(module, self.attr)
-
-
-class _SixMetaPathImporter(object):
-
- """
- A meta path importer to import six.moves and its submodules.
-
- This class implements a PEP302 finder and loader. It should be compatible
- with Python 2.5 and all existing versions of Python3
- """
-
- def __init__(self, six_module_name):
- self.name = six_module_name
- self.known_modules = {}
-
- def _add_module(self, mod, *fullnames):
- for fullname in fullnames:
- self.known_modules[self.name + "." + fullname] = mod
-
- def _get_module(self, fullname):
- return self.known_modules[self.name + "." + fullname]
-
- def find_module(self, fullname, path=None):
- if fullname in self.known_modules:
- return self
- return None
-
- def __get_module(self, fullname):
- try:
- return self.known_modules[fullname]
- except KeyError:
- raise ImportError("This loader does not know module " + fullname)
-
- def load_module(self, fullname):
- try:
- # in case of a reload
- return sys.modules[fullname]
- except KeyError:
- pass
- mod = self.__get_module(fullname)
- if isinstance(mod, MovedModule):
- mod = mod._resolve()
- else:
- mod.__loader__ = self
- sys.modules[fullname] = mod
- return mod
-
- def is_package(self, fullname):
- """
- Return true, if the named module is a package.
-
- We need this method to get correct spec objects with
- Python 3.4 (see PEP451)
- """
- return hasattr(self.__get_module(fullname), "__path__")
-
- def get_code(self, fullname):
- """Return None
-
- Required, if is_package is implemented"""
- self.__get_module(fullname) # eventually raises ImportError
- return None
- get_source = get_code # same as get_code
-
-_importer = _SixMetaPathImporter(__name__)
-
-
-class _MovedItems(_LazyModule):
-
- """Lazy loading of moved objects"""
- __path__ = [] # mark as package
-
-
-_moved_attributes = [
- MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
- MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
- MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
- MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
- MovedAttribute("intern", "__builtin__", "sys"),
- MovedAttribute("map", "itertools", "builtins", "imap", "map"),
- MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
- MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
- MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
- MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
- MovedAttribute("reduce", "__builtin__", "functools"),
- MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
- MovedAttribute("StringIO", "StringIO", "io"),
- MovedAttribute("UserDict", "UserDict", "collections"),
- MovedAttribute("UserList", "UserList", "collections"),
- MovedAttribute("UserString", "UserString", "collections"),
- MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
- MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
- MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
- MovedModule("builtins", "__builtin__"),
- MovedModule("configparser", "ConfigParser"),
- MovedModule("copyreg", "copy_reg"),
- MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
- MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
- MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
- MovedModule("http_cookies", "Cookie", "http.cookies"),
- MovedModule("html_entities", "htmlentitydefs", "html.entities"),
- MovedModule("html_parser", "HTMLParser", "html.parser"),
- MovedModule("http_client", "httplib", "http.client"),
- MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
- MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
- MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
- MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
- MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
- MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
- MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
- MovedModule("cPickle", "cPickle", "pickle"),
- MovedModule("queue", "Queue"),
- MovedModule("reprlib", "repr"),
- MovedModule("socketserver", "SocketServer"),
- MovedModule("_thread", "thread", "_thread"),
- MovedModule("tkinter", "Tkinter"),
- MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
- MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
- MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
- MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
- MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
- MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
- MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
- MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
- MovedModule("tkinter_colorchooser", "tkColorChooser",
- "tkinter.colorchooser"),
- MovedModule("tkinter_commondialog", "tkCommonDialog",
- "tkinter.commondialog"),
- MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
- MovedModule("tkinter_font", "tkFont", "tkinter.font"),
- MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
- MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
- "tkinter.simpledialog"),
- MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
- MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
- MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
- MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
- MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
- MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
-]
-# Add windows specific modules.
-if sys.platform == "win32":
- _moved_attributes += [
- MovedModule("winreg", "_winreg"),
- ]
-
-for attr in _moved_attributes:
- setattr(_MovedItems, attr.name, attr)
- if isinstance(attr, MovedModule):
- _importer._add_module(attr, "moves." + attr.name)
-del attr
-
-_MovedItems._moved_attributes = _moved_attributes
-
-moves = _MovedItems(__name__ + ".moves")
-_importer._add_module(moves, "moves")
-
-
-class Module_six_moves_urllib_parse(_LazyModule):
-
- """Lazy loading of moved objects in six.moves.urllib_parse"""
-
-
-_urllib_parse_moved_attributes = [
- MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
- MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
- MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
- MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
- MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
- MovedAttribute("urljoin", "urlparse", "urllib.parse"),
- MovedAttribute("urlparse", "urlparse", "urllib.parse"),
- MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
- MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
- MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
- MovedAttribute("quote", "urllib", "urllib.parse"),
- MovedAttribute("quote_plus", "urllib", "urllib.parse"),
- MovedAttribute("unquote", "urllib", "urllib.parse"),
- MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
- MovedAttribute("urlencode", "urllib", "urllib.parse"),
- MovedAttribute("splitquery", "urllib", "urllib.parse"),
- MovedAttribute("splittag", "urllib", "urllib.parse"),
- MovedAttribute("splituser", "urllib", "urllib.parse"),
- MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
- MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
- MovedAttribute("uses_params", "urlparse", "urllib.parse"),
- MovedAttribute("uses_query", "urlparse", "urllib.parse"),
- MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
-]
-for attr in _urllib_parse_moved_attributes:
- setattr(Module_six_moves_urllib_parse, attr.name, attr)
-del attr
-
-Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
-
-_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
- "moves.urllib_parse", "moves.urllib.parse")
-
-
-class Module_six_moves_urllib_error(_LazyModule):
-
- """Lazy loading of moved objects in six.moves.urllib_error"""
-
-
-_urllib_error_moved_attributes = [
- MovedAttribute("URLError", "urllib2", "urllib.error"),
- MovedAttribute("HTTPError", "urllib2", "urllib.error"),
- MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
-]
-for attr in _urllib_error_moved_attributes:
- setattr(Module_six_moves_urllib_error, attr.name, attr)
-del attr
-
-Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
-
-_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
- "moves.urllib_error", "moves.urllib.error")
-
-
-class Module_six_moves_urllib_request(_LazyModule):
-
- """Lazy loading of moved objects in six.moves.urllib_request"""
-
-
-_urllib_request_moved_attributes = [
- MovedAttribute("urlopen", "urllib2", "urllib.request"),
- MovedAttribute("install_opener", "urllib2", "urllib.request"),
- MovedAttribute("build_opener", "urllib2", "urllib.request"),
- MovedAttribute("pathname2url", "urllib", "urllib.request"),
- MovedAttribute("url2pathname", "urllib", "urllib.request"),
- MovedAttribute("getproxies", "urllib", "urllib.request"),
- MovedAttribute("Request", "urllib2", "urllib.request"),
- MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
- MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
- MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
- MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
- MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
- MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
- MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
- MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
- MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
- MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
- MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
- MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
- MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
- MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
- MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
- MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
- MovedAttribute("FileHandler", "urllib2", "urllib.request"),
- MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
- MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
- MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
- MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
- MovedAttribute("urlretrieve", "urllib", "urllib.request"),
- MovedAttribute("urlcleanup", "urllib", "urllib.request"),
- MovedAttribute("URLopener", "urllib", "urllib.request"),
- MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
- MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
-]
-for attr in _urllib_request_moved_attributes:
- setattr(Module_six_moves_urllib_request, attr.name, attr)
-del attr
-
-Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
-
-_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
- "moves.urllib_request", "moves.urllib.request")
-
-
-class Module_six_moves_urllib_response(_LazyModule):
-
- """Lazy loading of moved objects in six.moves.urllib_response"""
-
-
-_urllib_response_moved_attributes = [
- MovedAttribute("addbase", "urllib", "urllib.response"),
- MovedAttribute("addclosehook", "urllib", "urllib.response"),
- MovedAttribute("addinfo", "urllib", "urllib.response"),
- MovedAttribute("addinfourl", "urllib", "urllib.response"),
-]
-for attr in _urllib_response_moved_attributes:
- setattr(Module_six_moves_urllib_response, attr.name, attr)
-del attr
-
-Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
-
-_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
- "moves.urllib_response", "moves.urllib.response")
-
-
-class Module_six_moves_urllib_robotparser(_LazyModule):
-
- """Lazy loading of moved objects in six.moves.urllib_robotparser"""
-
-
-_urllib_robotparser_moved_attributes = [
- MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
-]
-for attr in _urllib_robotparser_moved_attributes:
- setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
-del attr
-
-Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
-
-_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
- "moves.urllib_robotparser", "moves.urllib.robotparser")
-
-
-class Module_six_moves_urllib(types.ModuleType):
-
- """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
- __path__ = [] # mark as package
- parse = _importer._get_module("moves.urllib_parse")
- error = _importer._get_module("moves.urllib_error")
- request = _importer._get_module("moves.urllib_request")
- response = _importer._get_module("moves.urllib_response")
- robotparser = _importer._get_module("moves.urllib_robotparser")
-
- def __dir__(self):
- return ['parse', 'error', 'request', 'response', 'robotparser']
-
-_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
- "moves.urllib")
-
-
-def add_move(move):
- """Add an item to six.moves."""
- setattr(_MovedItems, move.name, move)
-
-
-def remove_move(name):
- """Remove item from six.moves."""
- try:
- delattr(_MovedItems, name)
- except AttributeError:
- try:
- del moves.__dict__[name]
- except KeyError:
- raise AttributeError("no such move, %r" % (name,))
-
-
-if PY3:
- _meth_func = "__func__"
- _meth_self = "__self__"
-
- _func_closure = "__closure__"
- _func_code = "__code__"
- _func_defaults = "__defaults__"
- _func_globals = "__globals__"
-else:
- _meth_func = "im_func"
- _meth_self = "im_self"
-
- _func_closure = "func_closure"
- _func_code = "func_code"
- _func_defaults = "func_defaults"
- _func_globals = "func_globals"
-
-
-try:
- advance_iterator = next
-except NameError:
- def advance_iterator(it):
- return it.next()
-next = advance_iterator
-
-
-try:
- callable = callable
-except NameError:
- def callable(obj):
- return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
-
-
-if PY3:
- def get_unbound_function(unbound):
- return unbound
-
- create_bound_method = types.MethodType
-
- def create_unbound_method(func, cls):
- return func
-
- Iterator = object
-else:
- def get_unbound_function(unbound):
- return unbound.im_func
-
- def create_bound_method(func, obj):
- return types.MethodType(func, obj, obj.__class__)
-
- def create_unbound_method(func, cls):
- return types.MethodType(func, None, cls)
-
- class Iterator(object):
-
- def next(self):
- return type(self).__next__(self)
-
- callable = callable
-_add_doc(get_unbound_function,
- """Get the function out of a possibly unbound function""")
-
-
-get_method_function = operator.attrgetter(_meth_func)
-get_method_self = operator.attrgetter(_meth_self)
-get_function_closure = operator.attrgetter(_func_closure)
-get_function_code = operator.attrgetter(_func_code)
-get_function_defaults = operator.attrgetter(_func_defaults)
-get_function_globals = operator.attrgetter(_func_globals)
-
-
-if PY3:
- def iterkeys(d, **kw):
- return iter(d.keys(**kw))
-
- def itervalues(d, **kw):
- return iter(d.values(**kw))
-
- def iteritems(d, **kw):
- return iter(d.items(**kw))
-
- def iterlists(d, **kw):
- return iter(d.lists(**kw))
-
- viewkeys = operator.methodcaller("keys")
-
- viewvalues = operator.methodcaller("values")
-
- viewitems = operator.methodcaller("items")
-else:
- def iterkeys(d, **kw):
- return d.iterkeys(**kw)
-
- def itervalues(d, **kw):
- return d.itervalues(**kw)
-
- def iteritems(d, **kw):
- return d.iteritems(**kw)
-
- def iterlists(d, **kw):
- return d.iterlists(**kw)
-
- viewkeys = operator.methodcaller("viewkeys")
-
- viewvalues = operator.methodcaller("viewvalues")
-
- viewitems = operator.methodcaller("viewitems")
-
-_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
-_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
-_add_doc(iteritems,
- "Return an iterator over the (key, value) pairs of a dictionary.")
-_add_doc(iterlists,
- "Return an iterator over the (key, [values]) pairs of a dictionary.")
-
-
-if PY3:
- def b(s):
- return s.encode("latin-1")
-
- def u(s):
- return s
- unichr = chr
- import struct
- int2byte = struct.Struct(">B").pack
- del struct
- byte2int = operator.itemgetter(0)
- indexbytes = operator.getitem
- iterbytes = iter
- import io
- StringIO = io.StringIO
- BytesIO = io.BytesIO
- _assertCountEqual = "assertCountEqual"
- if sys.version_info[1] <= 1:
- _assertRaisesRegex = "assertRaisesRegexp"
- _assertRegex = "assertRegexpMatches"
- else:
- _assertRaisesRegex = "assertRaisesRegex"
- _assertRegex = "assertRegex"
-else:
- def b(s):
- return s
- # Workaround for standalone backslash
-
- def u(s):
- return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
- unichr = unichr
- int2byte = chr
-
- def byte2int(bs):
- return ord(bs[0])
-
- def indexbytes(buf, i):
- return ord(buf[i])
- iterbytes = functools.partial(itertools.imap, ord)
- import StringIO
- StringIO = BytesIO = StringIO.StringIO
- _assertCountEqual = "assertItemsEqual"
- _assertRaisesRegex = "assertRaisesRegexp"
- _assertRegex = "assertRegexpMatches"
-_add_doc(b, """Byte literal""")
-_add_doc(u, """Text literal""")
-
-
-def assertCountEqual(self, *args, **kwargs):
- return getattr(self, _assertCountEqual)(*args, **kwargs)
-
-
-def assertRaisesRegex(self, *args, **kwargs):
- return getattr(self, _assertRaisesRegex)(*args, **kwargs)
-
-
-def assertRegex(self, *args, **kwargs):
- return getattr(self, _assertRegex)(*args, **kwargs)
-
-
-if PY3:
- exec_ = getattr(moves.builtins, "exec")
-
- def reraise(tp, value, tb=None):
- if value is None:
- value = tp()
- if value.__traceback__ is not tb:
- raise value.with_traceback(tb)
- raise value
-
-else:
- def exec_(_code_, _globs_=None, _locs_=None):
- """Execute code in a namespace."""
- if _globs_ is None:
- frame = sys._getframe(1)
- _globs_ = frame.f_globals
- if _locs_ is None:
- _locs_ = frame.f_locals
- del frame
- elif _locs_ is None:
- _locs_ = _globs_
- exec("""exec _code_ in _globs_, _locs_""")
-
- exec_("""def reraise(tp, value, tb=None):
- raise tp, value, tb
-""")
-
-
-if sys.version_info[:2] == (3, 2):
- exec_("""def raise_from(value, from_value):
- if from_value is None:
- raise value
- raise value from from_value
-""")
-elif sys.version_info[:2] > (3, 2):
- exec_("""def raise_from(value, from_value):
- raise value from from_value
-""")
-else:
- def raise_from(value, from_value):
- raise value
-
-
-print_ = getattr(moves.builtins, "print", None)
-if print_ is None:
- def print_(*args, **kwargs):
- """The new-style print function for Python 2.4 and 2.5."""
- fp = kwargs.pop("file", sys.stdout)
- if fp is None:
- return
-
- def write(data):
- if not isinstance(data, basestring):
- data = str(data)
- # If the file has an encoding, encode unicode with it.
- if (isinstance(fp, file) and
- isinstance(data, unicode) and
- fp.encoding is not None):
- errors = getattr(fp, "errors", None)
- if errors is None:
- errors = "strict"
- data = data.encode(fp.encoding, errors)
- fp.write(data)
- want_unicode = False
- sep = kwargs.pop("sep", None)
- if sep is not None:
- if isinstance(sep, unicode):
- want_unicode = True
- elif not isinstance(sep, str):
- raise TypeError("sep must be None or a string")
- end = kwargs.pop("end", None)
- if end is not None:
- if isinstance(end, unicode):
- want_unicode = True
- elif not isinstance(end, str):
- raise TypeError("end must be None or a string")
- if kwargs:
- raise TypeError("invalid keyword arguments to print()")
- if not want_unicode:
- for arg in args:
- if isinstance(arg, unicode):
- want_unicode = True
- break
- if want_unicode:
- newline = unicode("\n")
- space = unicode(" ")
- else:
- newline = "\n"
- space = " "
- if sep is None:
- sep = space
- if end is None:
- end = newline
- for i, arg in enumerate(args):
- if i:
- write(sep)
- write(arg)
- write(end)
-if sys.version_info[:2] < (3, 3):
- _print = print_
-
- def print_(*args, **kwargs):
- fp = kwargs.get("file", sys.stdout)
- flush = kwargs.pop("flush", False)
- _print(*args, **kwargs)
- if flush and fp is not None:
- fp.flush()
-
-_add_doc(reraise, """Reraise an exception.""")
-
-if sys.version_info[0:2] < (3, 4):
- def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
- updated=functools.WRAPPER_UPDATES):
- def wrapper(f):
- f = functools.wraps(wrapped, assigned, updated)(f)
- f.__wrapped__ = wrapped
- return f
- return wrapper
-else:
- wraps = functools.wraps
-
-
-def with_metaclass(meta, *bases):
- """Create a base class with a metaclass."""
- # This requires a bit of explanation: the basic idea is to make a dummy
- # metaclass for one level of class instantiation that replaces itself with
- # the actual metaclass.
- class metaclass(meta):
-
- def __new__(cls, name, this_bases, d):
- return meta(name, bases, d)
- return type.__new__(metaclass, 'temporary_class', (), {})
-
-
-def add_metaclass(metaclass):
- """Class decorator for creating a class with a metaclass."""
- def wrapper(cls):
- orig_vars = cls.__dict__.copy()
- slots = orig_vars.get('__slots__')
- if slots is not None:
- if isinstance(slots, str):
- slots = [slots]
- for slots_var in slots:
- orig_vars.pop(slots_var)
- orig_vars.pop('__dict__', None)
- orig_vars.pop('__weakref__', None)
- return metaclass(cls.__name__, cls.__bases__, orig_vars)
- return wrapper
-
-
-def python_2_unicode_compatible(klass):
- """
- A decorator that defines __unicode__ and __str__ methods under Python 2.
- Under Python 3 it does nothing.
-
- To support Python 2 and 3 with a single code base, define a __str__ method
- returning text and apply this decorator to the class.
- """
- if PY2:
- if '__str__' not in klass.__dict__:
- raise ValueError("@python_2_unicode_compatible cannot be applied "
- "to %s because it doesn't define __str__()." %
- klass.__name__)
- klass.__unicode__ = klass.__str__
- klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
- return klass
-
-
-# Complete the moves implementation.
-# This code is at the end of this module to speed up module loading.
-# Turn this module into a package.
-__path__ = [] # required for PEP 302 and PEP 451
-__package__ = __name__ # see PEP 366 @ReservedAssignment
-if globals().get("__spec__") is not None:
- __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
-# Remove other six meta path importers, since they cause problems. This can
-# happen if six is removed from sys.modules and then reloaded. (Setuptools does
-# this for some reason.)
-if sys.meta_path:
- for i, importer in enumerate(sys.meta_path):
- # Here's some real nastiness: Another "instance" of the six module might
- # be floating around. Therefore, we can't use isinstance() to check for
- # the six meta path importer, since the other six instance will have
- # inserted an importer with different class.
- if (type(importer).__name__ == "_SixMetaPathImporter" and
- importer.name == __name__):
- del sys.meta_path[i]
- break
- del i, importer
-# Finally, add the importer to the meta path import hook.
-sys.meta_path.append(_importer)
+"""Utilities for writing code that runs on Python 2 and 3"""
+
+# Copyright (c) 2010-2015 Benjamin Peterson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+from __future__ import absolute_import
+
+import functools
+import itertools
+import operator
+import sys
+import types
+
+__author__ = "Benjamin Peterson "
+__version__ = "1.10.0"
+
+
+# Useful for very coarse version differentiation.
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+PY34 = sys.version_info[0:2] >= (3, 4)
+
+if PY3:
+ string_types = str,
+ integer_types = int,
+ class_types = type,
+ text_type = str
+ binary_type = bytes
+
+ MAXSIZE = sys.maxsize
+else:
+ string_types = basestring,
+ integer_types = (int, long)
+ class_types = (type, types.ClassType)
+ text_type = unicode
+ binary_type = str
+
+ if sys.platform.startswith("java"):
+ # Jython always uses 32 bits.
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
+ class X(object):
+
+ def __len__(self):
+ return 1 << 31
+ try:
+ len(X())
+ except OverflowError:
+ # 32-bit
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # 64-bit
+ MAXSIZE = int((1 << 63) - 1)
+ del X
+
+
+def _add_doc(func, doc):
+ """Add documentation to a function."""
+ func.__doc__ = doc
+
+
+def _import_module(name):
+ """Import module, returning the module after the last dot."""
+ __import__(name)
+ return sys.modules[name]
+
+
+class _LazyDescr(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __get__(self, obj, tp):
+ result = self._resolve()
+ setattr(obj, self.name, result) # Invokes __set__.
+ try:
+ # This is a bit ugly, but it avoids running this again by
+ # removing this descriptor.
+ delattr(obj.__class__, self.name)
+ except AttributeError:
+ pass
+ return result
+
+
+class MovedModule(_LazyDescr):
+
+ def __init__(self, name, old, new=None):
+ super(MovedModule, self).__init__(name)
+ if PY3:
+ if new is None:
+ new = name
+ self.mod = new
+ else:
+ self.mod = old
+
+ def _resolve(self):
+ return _import_module(self.mod)
+
+ def __getattr__(self, attr):
+ _module = self._resolve()
+ value = getattr(_module, attr)
+ setattr(self, attr, value)
+ return value
+
+
+class _LazyModule(types.ModuleType):
+
+ def __init__(self, name):
+ super(_LazyModule, self).__init__(name)
+ self.__doc__ = self.__class__.__doc__
+
+ def __dir__(self):
+ attrs = ["__doc__", "__name__"]
+ attrs += [attr.name for attr in self._moved_attributes]
+ return attrs
+
+ # Subclasses should override this
+ _moved_attributes = []
+
+
+class MovedAttribute(_LazyDescr):
+
+ def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
+ super(MovedAttribute, self).__init__(name)
+ if PY3:
+ if new_mod is None:
+ new_mod = name
+ self.mod = new_mod
+ if new_attr is None:
+ if old_attr is None:
+ new_attr = name
+ else:
+ new_attr = old_attr
+ self.attr = new_attr
+ else:
+ self.mod = old_mod
+ if old_attr is None:
+ old_attr = name
+ self.attr = old_attr
+
+ def _resolve(self):
+ module = _import_module(self.mod)
+ return getattr(module, self.attr)
+
+
+class _SixMetaPathImporter(object):
+
+ """
+ A meta path importer to import six.moves and its submodules.
+
+ This class implements a PEP302 finder and loader. It should be compatible
+ with Python 2.5 and all existing versions of Python3
+ """
+
+ def __init__(self, six_module_name):
+ self.name = six_module_name
+ self.known_modules = {}
+
+ def _add_module(self, mod, *fullnames):
+ for fullname in fullnames:
+ self.known_modules[self.name + "." + fullname] = mod
+
+ def _get_module(self, fullname):
+ return self.known_modules[self.name + "." + fullname]
+
+ def find_module(self, fullname, path=None):
+ if fullname in self.known_modules:
+ return self
+ return None
+
+ def __get_module(self, fullname):
+ try:
+ return self.known_modules[fullname]
+ except KeyError:
+ raise ImportError("This loader does not know module " + fullname)
+
+ def load_module(self, fullname):
+ try:
+ # in case of a reload
+ return sys.modules[fullname]
+ except KeyError:
+ pass
+ mod = self.__get_module(fullname)
+ if isinstance(mod, MovedModule):
+ mod = mod._resolve()
+ else:
+ mod.__loader__ = self
+ sys.modules[fullname] = mod
+ return mod
+
+ def is_package(self, fullname):
+ """
+ Return true, if the named module is a package.
+
+ We need this method to get correct spec objects with
+ Python 3.4 (see PEP451)
+ """
+ return hasattr(self.__get_module(fullname), "__path__")
+
+ def get_code(self, fullname):
+ """Return None
+
+ Required, if is_package is implemented"""
+ self.__get_module(fullname) # eventually raises ImportError
+ return None
+ get_source = get_code # same as get_code
+
+_importer = _SixMetaPathImporter(__name__)
+
+
+class _MovedItems(_LazyModule):
+
+ """Lazy loading of moved objects"""
+ __path__ = [] # mark as package
+
+
+_moved_attributes = [
+ MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
+ MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
+ MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
+ MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
+ MovedAttribute("intern", "__builtin__", "sys"),
+ MovedAttribute("map", "itertools", "builtins", "imap", "map"),
+ MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
+ MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
+ MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
+ MovedAttribute("reduce", "__builtin__", "functools"),
+ MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
+ MovedAttribute("StringIO", "StringIO", "io"),
+ MovedAttribute("UserDict", "UserDict", "collections"),
+ MovedAttribute("UserList", "UserList", "collections"),
+ MovedAttribute("UserString", "UserString", "collections"),
+ MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
+ MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
+ MovedModule("builtins", "__builtin__"),
+ MovedModule("configparser", "ConfigParser"),
+ MovedModule("copyreg", "copy_reg"),
+ MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
+ MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
+ MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
+ MovedModule("http_cookies", "Cookie", "http.cookies"),
+ MovedModule("html_entities", "htmlentitydefs", "html.entities"),
+ MovedModule("html_parser", "HTMLParser", "html.parser"),
+ MovedModule("http_client", "httplib", "http.client"),
+ MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
+ MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
+ MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
+ MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
+ MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
+ MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
+ MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
+ MovedModule("cPickle", "cPickle", "pickle"),
+ MovedModule("queue", "Queue"),
+ MovedModule("reprlib", "repr"),
+ MovedModule("socketserver", "SocketServer"),
+ MovedModule("_thread", "thread", "_thread"),
+ MovedModule("tkinter", "Tkinter"),
+ MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
+ MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
+ MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
+ MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
+ MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
+ MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
+ MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
+ MovedModule("tkinter_colorchooser", "tkColorChooser",
+ "tkinter.colorchooser"),
+ MovedModule("tkinter_commondialog", "tkCommonDialog",
+ "tkinter.commondialog"),
+ MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_font", "tkFont", "tkinter.font"),
+ MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
+ MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
+ "tkinter.simpledialog"),
+ MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
+ MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
+ MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
+ MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
+ MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
+ MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
+]
+# Add windows specific modules.
+if sys.platform == "win32":
+ _moved_attributes += [
+ MovedModule("winreg", "_winreg"),
+ ]
+
+for attr in _moved_attributes:
+ setattr(_MovedItems, attr.name, attr)
+ if isinstance(attr, MovedModule):
+ _importer._add_module(attr, "moves." + attr.name)
+del attr
+
+_MovedItems._moved_attributes = _moved_attributes
+
+moves = _MovedItems(__name__ + ".moves")
+_importer._add_module(moves, "moves")
+
+
+class Module_six_moves_urllib_parse(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_parse"""
+
+
+_urllib_parse_moved_attributes = [
+ MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
+ MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
+ MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
+ MovedAttribute("urljoin", "urlparse", "urllib.parse"),
+ MovedAttribute("urlparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("quote", "urllib", "urllib.parse"),
+ MovedAttribute("quote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("unquote", "urllib", "urllib.parse"),
+ MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("urlencode", "urllib", "urllib.parse"),
+ MovedAttribute("splitquery", "urllib", "urllib.parse"),
+ MovedAttribute("splittag", "urllib", "urllib.parse"),
+ MovedAttribute("splituser", "urllib", "urllib.parse"),
+ MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_params", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_query", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
+]
+for attr in _urllib_parse_moved_attributes:
+ setattr(Module_six_moves_urllib_parse, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
+ "moves.urllib_parse", "moves.urllib.parse")
+
+
+class Module_six_moves_urllib_error(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_error"""
+
+
+_urllib_error_moved_attributes = [
+ MovedAttribute("URLError", "urllib2", "urllib.error"),
+ MovedAttribute("HTTPError", "urllib2", "urllib.error"),
+ MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
+]
+for attr in _urllib_error_moved_attributes:
+ setattr(Module_six_moves_urllib_error, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
+ "moves.urllib_error", "moves.urllib.error")
+
+
+class Module_six_moves_urllib_request(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_request"""
+
+
+_urllib_request_moved_attributes = [
+ MovedAttribute("urlopen", "urllib2", "urllib.request"),
+ MovedAttribute("install_opener", "urllib2", "urllib.request"),
+ MovedAttribute("build_opener", "urllib2", "urllib.request"),
+ MovedAttribute("pathname2url", "urllib", "urllib.request"),
+ MovedAttribute("url2pathname", "urllib", "urllib.request"),
+ MovedAttribute("getproxies", "urllib", "urllib.request"),
+ MovedAttribute("Request", "urllib2", "urllib.request"),
+ MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
+ MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FileHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("urlretrieve", "urllib", "urllib.request"),
+ MovedAttribute("urlcleanup", "urllib", "urllib.request"),
+ MovedAttribute("URLopener", "urllib", "urllib.request"),
+ MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
+ MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
+]
+for attr in _urllib_request_moved_attributes:
+ setattr(Module_six_moves_urllib_request, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
+ "moves.urllib_request", "moves.urllib.request")
+
+
+class Module_six_moves_urllib_response(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_response"""
+
+
+_urllib_response_moved_attributes = [
+ MovedAttribute("addbase", "urllib", "urllib.response"),
+ MovedAttribute("addclosehook", "urllib", "urllib.response"),
+ MovedAttribute("addinfo", "urllib", "urllib.response"),
+ MovedAttribute("addinfourl", "urllib", "urllib.response"),
+]
+for attr in _urllib_response_moved_attributes:
+ setattr(Module_six_moves_urllib_response, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
+ "moves.urllib_response", "moves.urllib.response")
+
+
+class Module_six_moves_urllib_robotparser(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_robotparser"""
+
+
+_urllib_robotparser_moved_attributes = [
+ MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
+]
+for attr in _urllib_robotparser_moved_attributes:
+ setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
+ "moves.urllib_robotparser", "moves.urllib.robotparser")
+
+
+class Module_six_moves_urllib(types.ModuleType):
+
+ """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
+ __path__ = [] # mark as package
+ parse = _importer._get_module("moves.urllib_parse")
+ error = _importer._get_module("moves.urllib_error")
+ request = _importer._get_module("moves.urllib_request")
+ response = _importer._get_module("moves.urllib_response")
+ robotparser = _importer._get_module("moves.urllib_robotparser")
+
+ def __dir__(self):
+ return ['parse', 'error', 'request', 'response', 'robotparser']
+
+_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
+ "moves.urllib")
+
+
+def add_move(move):
+ """Add an item to six.moves."""
+ setattr(_MovedItems, move.name, move)
+
+
+def remove_move(name):
+ """Remove item from six.moves."""
+ try:
+ delattr(_MovedItems, name)
+ except AttributeError:
+ try:
+ del moves.__dict__[name]
+ except KeyError:
+ raise AttributeError("no such move, %r" % (name,))
+
+
+if PY3:
+ _meth_func = "__func__"
+ _meth_self = "__self__"
+
+ _func_closure = "__closure__"
+ _func_code = "__code__"
+ _func_defaults = "__defaults__"
+ _func_globals = "__globals__"
+else:
+ _meth_func = "im_func"
+ _meth_self = "im_self"
+
+ _func_closure = "func_closure"
+ _func_code = "func_code"
+ _func_defaults = "func_defaults"
+ _func_globals = "func_globals"
+
+
+try:
+ advance_iterator = next
+except NameError:
+ def advance_iterator(it):
+ return it.next()
+next = advance_iterator
+
+
+try:
+ callable = callable
+except NameError:
+ def callable(obj):
+ return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
+
+
+if PY3:
+ def get_unbound_function(unbound):
+ return unbound
+
+ create_bound_method = types.MethodType
+
+ def create_unbound_method(func, cls):
+ return func
+
+ Iterator = object
+else:
+ def get_unbound_function(unbound):
+ return unbound.im_func
+
+ def create_bound_method(func, obj):
+ return types.MethodType(func, obj, obj.__class__)
+
+ def create_unbound_method(func, cls):
+ return types.MethodType(func, None, cls)
+
+ class Iterator(object):
+
+ def next(self):
+ return type(self).__next__(self)
+
+ callable = callable
+_add_doc(get_unbound_function,
+ """Get the function out of a possibly unbound function""")
+
+
+get_method_function = operator.attrgetter(_meth_func)
+get_method_self = operator.attrgetter(_meth_self)
+get_function_closure = operator.attrgetter(_func_closure)
+get_function_code = operator.attrgetter(_func_code)
+get_function_defaults = operator.attrgetter(_func_defaults)
+get_function_globals = operator.attrgetter(_func_globals)
+
+
+if PY3:
+ def iterkeys(d, **kw):
+ return iter(d.keys(**kw))
+
+ def itervalues(d, **kw):
+ return iter(d.values(**kw))
+
+ def iteritems(d, **kw):
+ return iter(d.items(**kw))
+
+ def iterlists(d, **kw):
+ return iter(d.lists(**kw))
+
+ viewkeys = operator.methodcaller("keys")
+
+ viewvalues = operator.methodcaller("values")
+
+ viewitems = operator.methodcaller("items")
+else:
+ def iterkeys(d, **kw):
+ return d.iterkeys(**kw)
+
+ def itervalues(d, **kw):
+ return d.itervalues(**kw)
+
+ def iteritems(d, **kw):
+ return d.iteritems(**kw)
+
+ def iterlists(d, **kw):
+ return d.iterlists(**kw)
+
+ viewkeys = operator.methodcaller("viewkeys")
+
+ viewvalues = operator.methodcaller("viewvalues")
+
+ viewitems = operator.methodcaller("viewitems")
+
+_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
+_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
+_add_doc(iteritems,
+ "Return an iterator over the (key, value) pairs of a dictionary.")
+_add_doc(iterlists,
+ "Return an iterator over the (key, [values]) pairs of a dictionary.")
+
+
+if PY3:
+ def b(s):
+ return s.encode("latin-1")
+
+ def u(s):
+ return s
+ unichr = chr
+ import struct
+ int2byte = struct.Struct(">B").pack
+ del struct
+ byte2int = operator.itemgetter(0)
+ indexbytes = operator.getitem
+ iterbytes = iter
+ import io
+ StringIO = io.StringIO
+ BytesIO = io.BytesIO
+ _assertCountEqual = "assertCountEqual"
+ if sys.version_info[1] <= 1:
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+ else:
+ _assertRaisesRegex = "assertRaisesRegex"
+ _assertRegex = "assertRegex"
+else:
+ def b(s):
+ return s
+ # Workaround for standalone backslash
+
+ def u(s):
+ return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
+ unichr = unichr
+ int2byte = chr
+
+ def byte2int(bs):
+ return ord(bs[0])
+
+ def indexbytes(buf, i):
+ return ord(buf[i])
+ iterbytes = functools.partial(itertools.imap, ord)
+ import StringIO
+ StringIO = BytesIO = StringIO.StringIO
+ _assertCountEqual = "assertItemsEqual"
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+_add_doc(b, """Byte literal""")
+_add_doc(u, """Text literal""")
+
+
+def assertCountEqual(self, *args, **kwargs):
+ return getattr(self, _assertCountEqual)(*args, **kwargs)
+
+
+def assertRaisesRegex(self, *args, **kwargs):
+ return getattr(self, _assertRaisesRegex)(*args, **kwargs)
+
+
+def assertRegex(self, *args, **kwargs):
+ return getattr(self, _assertRegex)(*args, **kwargs)
+
+
+if PY3:
+ exec_ = getattr(moves.builtins, "exec")
+
+ def reraise(tp, value, tb=None):
+ if value is None:
+ value = tp()
+ if value.__traceback__ is not tb:
+ raise value.with_traceback(tb)
+ raise value
+
+else:
+ def exec_(_code_, _globs_=None, _locs_=None):
+ """Execute code in a namespace."""
+ if _globs_ is None:
+ frame = sys._getframe(1)
+ _globs_ = frame.f_globals
+ if _locs_ is None:
+ _locs_ = frame.f_locals
+ del frame
+ elif _locs_ is None:
+ _locs_ = _globs_
+ exec("""exec _code_ in _globs_, _locs_""")
+
+ exec_("""def reraise(tp, value, tb=None):
+ raise tp, value, tb
+""")
+
+
+if sys.version_info[:2] == (3, 2):
+ exec_("""def raise_from(value, from_value):
+ if from_value is None:
+ raise value
+ raise value from from_value
+""")
+elif sys.version_info[:2] > (3, 2):
+ exec_("""def raise_from(value, from_value):
+ raise value from from_value
+""")
+else:
+ def raise_from(value, from_value):
+ raise value
+
+
+print_ = getattr(moves.builtins, "print", None)
+if print_ is None:
+ def print_(*args, **kwargs):
+ """The new-style print function for Python 2.4 and 2.5."""
+ fp = kwargs.pop("file", sys.stdout)
+ if fp is None:
+ return
+
+ def write(data):
+ if not isinstance(data, basestring):
+ data = str(data)
+ # If the file has an encoding, encode unicode with it.
+ if (isinstance(fp, file) and
+ isinstance(data, unicode) and
+ fp.encoding is not None):
+ errors = getattr(fp, "errors", None)
+ if errors is None:
+ errors = "strict"
+ data = data.encode(fp.encoding, errors)
+ fp.write(data)
+ want_unicode = False
+ sep = kwargs.pop("sep", None)
+ if sep is not None:
+ if isinstance(sep, unicode):
+ want_unicode = True
+ elif not isinstance(sep, str):
+ raise TypeError("sep must be None or a string")
+ end = kwargs.pop("end", None)
+ if end is not None:
+ if isinstance(end, unicode):
+ want_unicode = True
+ elif not isinstance(end, str):
+ raise TypeError("end must be None or a string")
+ if kwargs:
+ raise TypeError("invalid keyword arguments to print()")
+ if not want_unicode:
+ for arg in args:
+ if isinstance(arg, unicode):
+ want_unicode = True
+ break
+ if want_unicode:
+ newline = unicode("\n")
+ space = unicode(" ")
+ else:
+ newline = "\n"
+ space = " "
+ if sep is None:
+ sep = space
+ if end is None:
+ end = newline
+ for i, arg in enumerate(args):
+ if i:
+ write(sep)
+ write(arg)
+ write(end)
+if sys.version_info[:2] < (3, 3):
+ _print = print_
+
+ def print_(*args, **kwargs):
+ fp = kwargs.get("file", sys.stdout)
+ flush = kwargs.pop("flush", False)
+ _print(*args, **kwargs)
+ if flush and fp is not None:
+ fp.flush()
+
+_add_doc(reraise, """Reraise an exception.""")
+
+if sys.version_info[0:2] < (3, 4):
+ def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
+ updated=functools.WRAPPER_UPDATES):
+ def wrapper(f):
+ f = functools.wraps(wrapped, assigned, updated)(f)
+ f.__wrapped__ = wrapped
+ return f
+ return wrapper
+else:
+ wraps = functools.wraps
+
+
+def with_metaclass(meta, *bases):
+ """Create a base class with a metaclass."""
+ # This requires a bit of explanation: the basic idea is to make a dummy
+ # metaclass for one level of class instantiation that replaces itself with
+ # the actual metaclass.
+ class metaclass(meta):
+
+ def __new__(cls, name, this_bases, d):
+ return meta(name, bases, d)
+ return type.__new__(metaclass, 'temporary_class', (), {})
+
+
+def add_metaclass(metaclass):
+ """Class decorator for creating a class with a metaclass."""
+ def wrapper(cls):
+ orig_vars = cls.__dict__.copy()
+ slots = orig_vars.get('__slots__')
+ if slots is not None:
+ if isinstance(slots, str):
+ slots = [slots]
+ for slots_var in slots:
+ orig_vars.pop(slots_var)
+ orig_vars.pop('__dict__', None)
+ orig_vars.pop('__weakref__', None)
+ return metaclass(cls.__name__, cls.__bases__, orig_vars)
+ return wrapper
+
+
+def python_2_unicode_compatible(klass):
+ """
+ A decorator that defines __unicode__ and __str__ methods under Python 2.
+ Under Python 3 it does nothing.
+
+ To support Python 2 and 3 with a single code base, define a __str__ method
+ returning text and apply this decorator to the class.
+ """
+ if PY2:
+ if '__str__' not in klass.__dict__:
+ raise ValueError("@python_2_unicode_compatible cannot be applied "
+ "to %s because it doesn't define __str__()." %
+ klass.__name__)
+ klass.__unicode__ = klass.__str__
+ klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
+ return klass
+
+
+# Complete the moves implementation.
+# This code is at the end of this module to speed up module loading.
+# Turn this module into a package.
+__path__ = [] # required for PEP 302 and PEP 451
+__package__ = __name__ # see PEP 366 @ReservedAssignment
+if globals().get("__spec__") is not None:
+ __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
+# Remove other six meta path importers, since they cause problems. This can
+# happen if six is removed from sys.modules and then reloaded. (Setuptools does
+# this for some reason.)
+if sys.meta_path:
+ for i, importer in enumerate(sys.meta_path):
+ # Here's some real nastiness: Another "instance" of the six module might
+ # be floating around. Therefore, we can't use isinstance() to check for
+ # the six meta path importer, since the other six instance will have
+ # inserted an importer with different class.
+ if (type(importer).__name__ == "_SixMetaPathImporter" and
+ importer.name == __name__):
+ del sys.meta_path[i]
+ break
+ del i, importer
+# Finally, add the importer to the meta path import hook.
+sys.meta_path.append(_importer)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.p
similarity index 97%
rename from telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.py
rename to telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.p
index accb927..d6594eb 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/__init__.p
@@ -1,19 +1,19 @@
-import sys
-
-try:
- # Our match_hostname function is the same as 3.5's, so we only want to
- # import the match_hostname function if it's at least that good.
- if sys.version_info < (3, 5):
- raise ImportError("Fallback to vendored code")
-
- from ssl import CertificateError, match_hostname
-except ImportError:
- try:
- # Backport of the function from a pypi module
- from backports.ssl_match_hostname import CertificateError, match_hostname
- except ImportError:
- # Our vendored copy
- from ._implementation import CertificateError, match_hostname
-
-# Not needed, but documenting what we provide.
-__all__ = ('CertificateError', 'match_hostname')
+import sys
+
+try:
+ # Our match_hostname function is the same as 3.5's, so we only want to
+ # import the match_hostname function if it's at least that good.
+ if sys.version_info < (3, 5):
+ raise ImportError("Fallback to vendored code")
+
+ from ssl import CertificateError, match_hostname
+except ImportError:
+ try:
+ # Backport of the function from a pypi module
+ from backports.ssl_match_hostname import CertificateError, match_hostname
+ except ImportError:
+ # Our vendored copy
+ from ._implementation import CertificateError, match_hostname
+
+# Not needed, but documenting what we provide.
+__all__ = ('CertificateError', 'match_hostname')
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implementation.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implement
similarity index 97%
rename from telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implementation.py
rename to telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implement
index 1bb3e3e..1fd42f3 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implementation.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/packages/ssl_match_hostname/_implement
@@ -1,157 +1,157 @@
-"""The match_hostname() function from Python 3.3.3, essential when using SSL."""
-
-# Note: This file is under the PSF license as the code comes from the python
-# stdlib. http://docs.python.org/3/license.html
-
-import re
-import sys
-
-# ipaddress has been backported to 2.6+ in pypi. If it is installed on the
-# system, use it to handle IPAddress ServerAltnames (this was added in
-# python-3.5) otherwise only do DNS matching. This allows
-# backports.ssl_match_hostname to continue to be used all the way back to
-# python-2.4.
-try:
- import ipaddress
-except ImportError:
- ipaddress = None
-
-__version__ = '3.5.0.1'
-
-
-class CertificateError(ValueError):
- pass
-
-
-def _dnsname_match(dn, hostname, max_wildcards=1):
- """Matching according to RFC 6125, section 6.4.3
-
- http://tools.ietf.org/html/rfc6125#section-6.4.3
- """
- pats = []
- if not dn:
- return False
-
- # Ported from python3-syntax:
- # leftmost, *remainder = dn.split(r'.')
- parts = dn.split(r'.')
- leftmost = parts[0]
- remainder = parts[1:]
-
- wildcards = leftmost.count('*')
- if wildcards > max_wildcards:
- # Issue #17980: avoid denials of service by refusing more
- # than one wildcard per fragment. A survey of established
- # policy among SSL implementations showed it to be a
- # reasonable choice.
- raise CertificateError(
- "too many wildcards in certificate DNS name: " + repr(dn))
-
- # speed up common case w/o wildcards
- if not wildcards:
- return dn.lower() == hostname.lower()
-
- # RFC 6125, section 6.4.3, subitem 1.
- # The client SHOULD NOT attempt to match a presented identifier in which
- # the wildcard character comprises a label other than the left-most label.
- if leftmost == '*':
- # When '*' is a fragment by itself, it matches a non-empty dotless
- # fragment.
- pats.append('[^.]+')
- elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
- # RFC 6125, section 6.4.3, subitem 3.
- # The client SHOULD NOT attempt to match a presented identifier
- # where the wildcard character is embedded within an A-label or
- # U-label of an internationalized domain name.
- pats.append(re.escape(leftmost))
- else:
- # Otherwise, '*' matches any dotless string, e.g. www*
- pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
-
- # add the remaining fragments, ignore any wildcards
- for frag in remainder:
- pats.append(re.escape(frag))
-
- pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
- return pat.match(hostname)
-
-
-def _to_unicode(obj):
- if isinstance(obj, str) and sys.version_info < (3,):
- obj = unicode(obj, encoding='ascii', errors='strict')
- return obj
-
-def _ipaddress_match(ipname, host_ip):
- """Exact matching of IP addresses.
-
- RFC 6125 explicitly doesn't define an algorithm for this
- (section 1.7.2 - "Out of Scope").
- """
- # OpenSSL may add a trailing newline to a subjectAltName's IP address
- # Divergence from upstream: ipaddress can't handle byte str
- ip = ipaddress.ip_address(_to_unicode(ipname).rstrip())
- return ip == host_ip
-
-
-def match_hostname(cert, hostname):
- """Verify that *cert* (in decoded format as returned by
- SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
- rules are followed, but IP addresses are not accepted for *hostname*.
-
- CertificateError is raised on failure. On success, the function
- returns nothing.
- """
- if not cert:
- raise ValueError("empty or no certificate, match_hostname needs a "
- "SSL socket or SSL context with either "
- "CERT_OPTIONAL or CERT_REQUIRED")
- try:
- # Divergence from upstream: ipaddress can't handle byte str
- host_ip = ipaddress.ip_address(_to_unicode(hostname))
- except ValueError:
- # Not an IP address (common case)
- host_ip = None
- except UnicodeError:
- # Divergence from upstream: Have to deal with ipaddress not taking
- # byte strings. addresses should be all ascii, so we consider it not
- # an ipaddress in this case
- host_ip = None
- except AttributeError:
- # Divergence from upstream: Make ipaddress library optional
- if ipaddress is None:
- host_ip = None
- else:
- raise
- dnsnames = []
- san = cert.get('subjectAltName', ())
- for key, value in san:
- if key == 'DNS':
- if host_ip is None and _dnsname_match(value, hostname):
- return
- dnsnames.append(value)
- elif key == 'IP Address':
- if host_ip is not None and _ipaddress_match(value, host_ip):
- return
- dnsnames.append(value)
- if not dnsnames:
- # The subject is only checked when there is no dNSName entry
- # in subjectAltName
- for sub in cert.get('subject', ()):
- for key, value in sub:
- # XXX according to RFC 2818, the most specific Common Name
- # must be used.
- if key == 'commonName':
- if _dnsname_match(value, hostname):
- return
- dnsnames.append(value)
- if len(dnsnames) > 1:
- raise CertificateError("hostname %r "
- "doesn't match either of %s"
- % (hostname, ', '.join(map(repr, dnsnames))))
- elif len(dnsnames) == 1:
- raise CertificateError("hostname %r "
- "doesn't match %r"
- % (hostname, dnsnames[0]))
- else:
- raise CertificateError("no appropriate commonName or "
- "subjectAltName fields were found")
+"""The match_hostname() function from Python 3.3.3, essential when using SSL."""
+
+# Note: This file is under the PSF license as the code comes from the python
+# stdlib. http://docs.python.org/3/license.html
+
+import re
+import sys
+
+# ipaddress has been backported to 2.6+ in pypi. If it is installed on the
+# system, use it to handle IPAddress ServerAltnames (this was added in
+# python-3.5) otherwise only do DNS matching. This allows
+# backports.ssl_match_hostname to continue to be used all the way back to
+# python-2.4.
+try:
+ import ipaddress
+except ImportError:
+ ipaddress = None
+
+__version__ = '3.5.0.1'
+
+
+class CertificateError(ValueError):
+ pass
+
+
+def _dnsname_match(dn, hostname, max_wildcards=1):
+ """Matching according to RFC 6125, section 6.4.3
+
+ http://tools.ietf.org/html/rfc6125#section-6.4.3
+ """
+ pats = []
+ if not dn:
+ return False
+
+ # Ported from python3-syntax:
+ # leftmost, *remainder = dn.split(r'.')
+ parts = dn.split(r'.')
+ leftmost = parts[0]
+ remainder = parts[1:]
+
+ wildcards = leftmost.count('*')
+ if wildcards > max_wildcards:
+ # Issue #17980: avoid denials of service by refusing more
+ # than one wildcard per fragment. A survey of established
+ # policy among SSL implementations showed it to be a
+ # reasonable choice.
+ raise CertificateError(
+ "too many wildcards in certificate DNS name: " + repr(dn))
+
+ # speed up common case w/o wildcards
+ if not wildcards:
+ return dn.lower() == hostname.lower()
+
+ # RFC 6125, section 6.4.3, subitem 1.
+ # The client SHOULD NOT attempt to match a presented identifier in which
+ # the wildcard character comprises a label other than the left-most label.
+ if leftmost == '*':
+ # When '*' is a fragment by itself, it matches a non-empty dotless
+ # fragment.
+ pats.append('[^.]+')
+ elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
+ # RFC 6125, section 6.4.3, subitem 3.
+ # The client SHOULD NOT attempt to match a presented identifier
+ # where the wildcard character is embedded within an A-label or
+ # U-label of an internationalized domain name.
+ pats.append(re.escape(leftmost))
+ else:
+ # Otherwise, '*' matches any dotless string, e.g. www*
+ pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
+
+ # add the remaining fragments, ignore any wildcards
+ for frag in remainder:
+ pats.append(re.escape(frag))
+
+ pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
+ return pat.match(hostname)
+
+
+def _to_unicode(obj):
+ if isinstance(obj, str) and sys.version_info < (3,):
+ obj = unicode(obj, encoding='ascii', errors='strict')
+ return obj
+
+def _ipaddress_match(ipname, host_ip):
+ """Exact matching of IP addresses.
+
+ RFC 6125 explicitly doesn't define an algorithm for this
+ (section 1.7.2 - "Out of Scope").
+ """
+ # OpenSSL may add a trailing newline to a subjectAltName's IP address
+ # Divergence from upstream: ipaddress can't handle byte str
+ ip = ipaddress.ip_address(_to_unicode(ipname).rstrip())
+ return ip == host_ip
+
+
+def match_hostname(cert, hostname):
+ """Verify that *cert* (in decoded format as returned by
+ SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
+ rules are followed, but IP addresses are not accepted for *hostname*.
+
+ CertificateError is raised on failure. On success, the function
+ returns nothing.
+ """
+ if not cert:
+ raise ValueError("empty or no certificate, match_hostname needs a "
+ "SSL socket or SSL context with either "
+ "CERT_OPTIONAL or CERT_REQUIRED")
+ try:
+ # Divergence from upstream: ipaddress can't handle byte str
+ host_ip = ipaddress.ip_address(_to_unicode(hostname))
+ except ValueError:
+ # Not an IP address (common case)
+ host_ip = None
+ except UnicodeError:
+ # Divergence from upstream: Have to deal with ipaddress not taking
+ # byte strings. addresses should be all ascii, so we consider it not
+ # an ipaddress in this case
+ host_ip = None
+ except AttributeError:
+ # Divergence from upstream: Make ipaddress library optional
+ if ipaddress is None:
+ host_ip = None
+ else:
+ raise
+ dnsnames = []
+ san = cert.get('subjectAltName', ())
+ for key, value in san:
+ if key == 'DNS':
+ if host_ip is None and _dnsname_match(value, hostname):
+ return
+ dnsnames.append(value)
+ elif key == 'IP Address':
+ if host_ip is not None and _ipaddress_match(value, host_ip):
+ return
+ dnsnames.append(value)
+ if not dnsnames:
+ # The subject is only checked when there is no dNSName entry
+ # in subjectAltName
+ for sub in cert.get('subject', ()):
+ for key, value in sub:
+ # XXX according to RFC 2818, the most specific Common Name
+ # must be used.
+ if key == 'commonName':
+ if _dnsname_match(value, hostname):
+ return
+ dnsnames.append(value)
+ if len(dnsnames) > 1:
+ raise CertificateError("hostname %r "
+ "doesn't match either of %s"
+ % (hostname, ', '.join(map(repr, dnsnames))))
+ elif len(dnsnames) == 1:
+ raise CertificateError("hostname %r "
+ "doesn't match %r"
+ % (hostname, dnsnames[0]))
+ else:
+ raise CertificateError("no appropriate commonName or "
+ "subjectAltName fields were found")
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/poolmanager.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/poolmanager.py
index c59935b..cc5a00e 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/poolmanager.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/poolmanager.py
@@ -1,363 +1,363 @@
-from __future__ import absolute_import
-import collections
-import functools
-import logging
-
-from ._collections import RecentlyUsedContainer
-from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool
-from .connectionpool import port_by_scheme
-from .exceptions import LocationValueError, MaxRetryError, ProxySchemeUnknown
-from .packages.six.moves.urllib.parse import urljoin
-from .request import RequestMethods
-from .util.url import parse_url
-from .util.retry import Retry
-
-
-__all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url']
-
-
-log = logging.getLogger(__name__)
-
-SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs',
- 'ssl_version', 'ca_cert_dir', 'ssl_context')
-
-# The base fields to use when determining what pool to get a connection from;
-# these do not rely on the ``connection_pool_kw`` and can be determined by the
-# URL and potentially the ``urllib3.connection.port_by_scheme`` dictionary.
-#
-# All custom key schemes should include the fields in this key at a minimum.
-BasePoolKey = collections.namedtuple('BasePoolKey', ('scheme', 'host', 'port'))
-
-# The fields to use when determining what pool to get a HTTP and HTTPS
-# connection from. All additional fields must be present in the PoolManager's
-# ``connection_pool_kw`` instance variable.
-HTTPPoolKey = collections.namedtuple(
- 'HTTPPoolKey', BasePoolKey._fields + ('timeout', 'retries', 'strict',
- 'block', 'source_address')
-)
-HTTPSPoolKey = collections.namedtuple(
- 'HTTPSPoolKey', HTTPPoolKey._fields + SSL_KEYWORDS
-)
-
-
-def _default_key_normalizer(key_class, request_context):
- """
- Create a pool key of type ``key_class`` for a request.
-
- According to RFC 3986, both the scheme and host are case-insensitive.
- Therefore, this function normalizes both before constructing the pool
- key for an HTTPS request. If you wish to change this behaviour, provide
- alternate callables to ``key_fn_by_scheme``.
-
- :param key_class:
- The class to use when constructing the key. This should be a namedtuple
- with the ``scheme`` and ``host`` keys at a minimum.
-
- :param request_context:
- A dictionary-like object that contain the context for a request.
- It should contain a key for each field in the :class:`HTTPPoolKey`
- """
- context = {}
- for key in key_class._fields:
- context[key] = request_context.get(key)
- context['scheme'] = context['scheme'].lower()
- context['host'] = context['host'].lower()
- return key_class(**context)
-
-
-# A dictionary that maps a scheme to a callable that creates a pool key.
-# This can be used to alter the way pool keys are constructed, if desired.
-# Each PoolManager makes a copy of this dictionary so they can be configured
-# globally here, or individually on the instance.
-key_fn_by_scheme = {
- 'http': functools.partial(_default_key_normalizer, HTTPPoolKey),
- 'https': functools.partial(_default_key_normalizer, HTTPSPoolKey),
-}
-
-pool_classes_by_scheme = {
- 'http': HTTPConnectionPool,
- 'https': HTTPSConnectionPool,
-}
-
-
-class PoolManager(RequestMethods):
- """
- Allows for arbitrary requests while transparently keeping track of
- necessary connection pools for you.
-
- :param num_pools:
- Number of connection pools to cache before discarding the least
- recently used pool.
-
- :param headers:
- Headers to include with all requests, unless other headers are given
- explicitly.
-
- :param \\**connection_pool_kw:
- Additional parameters are used to create fresh
- :class:`urllib3.connectionpool.ConnectionPool` instances.
-
- Example::
-
- >>> manager = PoolManager(num_pools=2)
- >>> r = manager.request('GET', 'http://google.com/')
- >>> r = manager.request('GET', 'http://google.com/mail')
- >>> r = manager.request('GET', 'http://yahoo.com/')
- >>> len(manager.pools)
- 2
-
- """
-
- proxy = None
-
- def __init__(self, num_pools=10, headers=None, **connection_pool_kw):
- RequestMethods.__init__(self, headers)
- self.connection_pool_kw = connection_pool_kw
- self.pools = RecentlyUsedContainer(num_pools,
- dispose_func=lambda p: p.close())
-
- # Locally set the pool classes and keys so other PoolManagers can
- # override them.
- self.pool_classes_by_scheme = pool_classes_by_scheme
- self.key_fn_by_scheme = key_fn_by_scheme.copy()
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.clear()
- # Return False to re-raise any potential exceptions
- return False
-
- def _new_pool(self, scheme, host, port):
- """
- Create a new :class:`ConnectionPool` based on host, port and scheme.
-
- This method is used to actually create the connection pools handed out
- by :meth:`connection_from_url` and companion methods. It is intended
- to be overridden for customization.
- """
- pool_cls = self.pool_classes_by_scheme[scheme]
- kwargs = self.connection_pool_kw
- if scheme == 'http':
- kwargs = self.connection_pool_kw.copy()
- for kw in SSL_KEYWORDS:
- kwargs.pop(kw, None)
-
- return pool_cls(host, port, **kwargs)
-
- def clear(self):
- """
- Empty our store of pools and direct them all to close.
-
- This will not affect in-flight connections, but they will not be
- re-used after completion.
- """
- self.pools.clear()
-
- def connection_from_host(self, host, port=None, scheme='http'):
- """
- Get a :class:`ConnectionPool` based on the host, port, and scheme.
-
- If ``port`` isn't given, it will be derived from the ``scheme`` using
- ``urllib3.connectionpool.port_by_scheme``.
- """
-
- if not host:
- raise LocationValueError("No host specified.")
-
- request_context = self.connection_pool_kw.copy()
- request_context['scheme'] = scheme or 'http'
- if not port:
- port = port_by_scheme.get(request_context['scheme'].lower(), 80)
- request_context['port'] = port
- request_context['host'] = host
-
- return self.connection_from_context(request_context)
-
- def connection_from_context(self, request_context):
- """
- Get a :class:`ConnectionPool` based on the request context.
-
- ``request_context`` must at least contain the ``scheme`` key and its
- value must be a key in ``key_fn_by_scheme`` instance variable.
- """
- scheme = request_context['scheme'].lower()
- pool_key_constructor = self.key_fn_by_scheme[scheme]
- pool_key = pool_key_constructor(request_context)
-
- return self.connection_from_pool_key(pool_key)
-
- def connection_from_pool_key(self, pool_key):
- """
- Get a :class:`ConnectionPool` based on the provided pool key.
-
- ``pool_key`` should be a namedtuple that only contains immutable
- objects. At a minimum it must have the ``scheme``, ``host``, and
- ``port`` fields.
- """
- with self.pools.lock:
- # If the scheme, host, or port doesn't match existing open
- # connections, open a new ConnectionPool.
- pool = self.pools.get(pool_key)
- if pool:
- return pool
-
- # Make a fresh ConnectionPool of the desired type
- pool = self._new_pool(pool_key.scheme, pool_key.host, pool_key.port)
- self.pools[pool_key] = pool
-
- return pool
-
- def connection_from_url(self, url):
- """
- Similar to :func:`urllib3.connectionpool.connection_from_url` but
- doesn't pass any additional parameters to the
- :class:`urllib3.connectionpool.ConnectionPool` constructor.
-
- Additional parameters are taken from the :class:`.PoolManager`
- constructor.
- """
- u = parse_url(url)
- return self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
-
- def urlopen(self, method, url, redirect=True, **kw):
- """
- Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen`
- with custom cross-host redirect logic and only sends the request-uri
- portion of the ``url``.
-
- The given ``url`` parameter must be absolute, such that an appropriate
- :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it.
- """
- u = parse_url(url)
- conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
-
- kw['assert_same_host'] = False
- kw['redirect'] = False
- if 'headers' not in kw:
- kw['headers'] = self.headers
-
- if self.proxy is not None and u.scheme == "http":
- response = conn.urlopen(method, url, **kw)
- else:
- response = conn.urlopen(method, u.request_uri, **kw)
-
- redirect_location = redirect and response.get_redirect_location()
- if not redirect_location:
- return response
-
- # Support relative URLs for redirecting.
- redirect_location = urljoin(url, redirect_location)
-
- # RFC 7231, Section 6.4.4
- if response.status == 303:
- method = 'GET'
-
- retries = kw.get('retries')
- if not isinstance(retries, Retry):
- retries = Retry.from_int(retries, redirect=redirect)
-
- try:
- retries = retries.increment(method, url, response=response, _pool=conn)
- except MaxRetryError:
- if retries.raise_on_redirect:
- raise
- return response
-
- kw['retries'] = retries
- kw['redirect'] = redirect
-
- log.info("Redirecting %s -> %s", url, redirect_location)
- return self.urlopen(method, redirect_location, **kw)
-
-
-class ProxyManager(PoolManager):
- """
- Behaves just like :class:`PoolManager`, but sends all requests through
- the defined proxy, using the CONNECT method for HTTPS URLs.
-
- :param proxy_url:
- The URL of the proxy to be used.
-
- :param proxy_headers:
- A dictionary contaning headers that will be sent to the proxy. In case
- of HTTP they are being sent with each request, while in the
- HTTPS/CONNECT case they are sent only once. Could be used for proxy
- authentication.
-
- Example:
- >>> proxy = urllib3.ProxyManager('http://localhost:3128/')
- >>> r1 = proxy.request('GET', 'http://google.com/')
- >>> r2 = proxy.request('GET', 'http://httpbin.org/')
- >>> len(proxy.pools)
- 1
- >>> r3 = proxy.request('GET', 'https://httpbin.org/')
- >>> r4 = proxy.request('GET', 'https://twitter.com/')
- >>> len(proxy.pools)
- 3
-
- """
-
- def __init__(self, proxy_url, num_pools=10, headers=None,
- proxy_headers=None, **connection_pool_kw):
-
- if isinstance(proxy_url, HTTPConnectionPool):
- proxy_url = '%s://%s:%i' % (proxy_url.scheme, proxy_url.host,
- proxy_url.port)
- proxy = parse_url(proxy_url)
- if not proxy.port:
- port = port_by_scheme.get(proxy.scheme, 80)
- proxy = proxy._replace(port=port)
-
- if proxy.scheme not in ("http", "https"):
- raise ProxySchemeUnknown(proxy.scheme)
-
- self.proxy = proxy
- self.proxy_headers = proxy_headers or {}
-
- connection_pool_kw['_proxy'] = self.proxy
- connection_pool_kw['_proxy_headers'] = self.proxy_headers
-
- super(ProxyManager, self).__init__(
- num_pools, headers, **connection_pool_kw)
-
- def connection_from_host(self, host, port=None, scheme='http'):
- if scheme == "https":
- return super(ProxyManager, self).connection_from_host(
- host, port, scheme)
-
- return super(ProxyManager, self).connection_from_host(
- self.proxy.host, self.proxy.port, self.proxy.scheme)
-
- def _set_proxy_headers(self, url, headers=None):
- """
- Sets headers needed by proxies: specifically, the Accept and Host
- headers. Only sets headers not provided by the user.
- """
- headers_ = {'Accept': '*/*'}
-
- netloc = parse_url(url).netloc
- if netloc:
- headers_['Host'] = netloc
-
- if headers:
- headers_.update(headers)
- return headers_
-
- def urlopen(self, method, url, redirect=True, **kw):
- "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute."
- u = parse_url(url)
-
- if u.scheme == "http":
- # For proxied HTTPS requests, httplib sets the necessary headers
- # on the CONNECT to the proxy. For HTTP, we'll definitely
- # need to set 'Host' at the very least.
- headers = kw.get('headers', self.headers)
- kw['headers'] = self._set_proxy_headers(url, headers)
-
- return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw)
-
-
-def proxy_from_url(url, **kw):
- return ProxyManager(proxy_url=url, **kw)
+from __future__ import absolute_import
+import collections
+import functools
+import logging
+
+from ._collections import RecentlyUsedContainer
+from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool
+from .connectionpool import port_by_scheme
+from .exceptions import LocationValueError, MaxRetryError, ProxySchemeUnknown
+from .packages.six.moves.urllib.parse import urljoin
+from .request import RequestMethods
+from .util.url import parse_url
+from .util.retry import Retry
+
+
+__all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url']
+
+
+log = logging.getLogger(__name__)
+
+SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs',
+ 'ssl_version', 'ca_cert_dir', 'ssl_context')
+
+# The base fields to use when determining what pool to get a connection from;
+# these do not rely on the ``connection_pool_kw`` and can be determined by the
+# URL and potentially the ``urllib3.connection.port_by_scheme`` dictionary.
+#
+# All custom key schemes should include the fields in this key at a minimum.
+BasePoolKey = collections.namedtuple('BasePoolKey', ('scheme', 'host', 'port'))
+
+# The fields to use when determining what pool to get a HTTP and HTTPS
+# connection from. All additional fields must be present in the PoolManager's
+# ``connection_pool_kw`` instance variable.
+HTTPPoolKey = collections.namedtuple(
+ 'HTTPPoolKey', BasePoolKey._fields + ('timeout', 'retries', 'strict',
+ 'block', 'source_address')
+)
+HTTPSPoolKey = collections.namedtuple(
+ 'HTTPSPoolKey', HTTPPoolKey._fields + SSL_KEYWORDS
+)
+
+
+def _default_key_normalizer(key_class, request_context):
+ """
+ Create a pool key of type ``key_class`` for a request.
+
+ According to RFC 3986, both the scheme and host are case-insensitive.
+ Therefore, this function normalizes both before constructing the pool
+ key for an HTTPS request. If you wish to change this behaviour, provide
+ alternate callables to ``key_fn_by_scheme``.
+
+ :param key_class:
+ The class to use when constructing the key. This should be a namedtuple
+ with the ``scheme`` and ``host`` keys at a minimum.
+
+ :param request_context:
+ A dictionary-like object that contain the context for a request.
+ It should contain a key for each field in the :class:`HTTPPoolKey`
+ """
+ context = {}
+ for key in key_class._fields:
+ context[key] = request_context.get(key)
+ context['scheme'] = context['scheme'].lower()
+ context['host'] = context['host'].lower()
+ return key_class(**context)
+
+
+# A dictionary that maps a scheme to a callable that creates a pool key.
+# This can be used to alter the way pool keys are constructed, if desired.
+# Each PoolManager makes a copy of this dictionary so they can be configured
+# globally here, or individually on the instance.
+key_fn_by_scheme = {
+ 'http': functools.partial(_default_key_normalizer, HTTPPoolKey),
+ 'https': functools.partial(_default_key_normalizer, HTTPSPoolKey),
+}
+
+pool_classes_by_scheme = {
+ 'http': HTTPConnectionPool,
+ 'https': HTTPSConnectionPool,
+}
+
+
+class PoolManager(RequestMethods):
+ """
+ Allows for arbitrary requests while transparently keeping track of
+ necessary connection pools for you.
+
+ :param num_pools:
+ Number of connection pools to cache before discarding the least
+ recently used pool.
+
+ :param headers:
+ Headers to include with all requests, unless other headers are given
+ explicitly.
+
+ :param \\**connection_pool_kw:
+ Additional parameters are used to create fresh
+ :class:`urllib3.connectionpool.ConnectionPool` instances.
+
+ Example::
+
+ >>> manager = PoolManager(num_pools=2)
+ >>> r = manager.request('GET', 'http://google.com/')
+ >>> r = manager.request('GET', 'http://google.com/mail')
+ >>> r = manager.request('GET', 'http://yahoo.com/')
+ >>> len(manager.pools)
+ 2
+
+ """
+
+ proxy = None
+
+ def __init__(self, num_pools=10, headers=None, **connection_pool_kw):
+ RequestMethods.__init__(self, headers)
+ self.connection_pool_kw = connection_pool_kw
+ self.pools = RecentlyUsedContainer(num_pools,
+ dispose_func=lambda p: p.close())
+
+ # Locally set the pool classes and keys so other PoolManagers can
+ # override them.
+ self.pool_classes_by_scheme = pool_classes_by_scheme
+ self.key_fn_by_scheme = key_fn_by_scheme.copy()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.clear()
+ # Return False to re-raise any potential exceptions
+ return False
+
+ def _new_pool(self, scheme, host, port):
+ """
+ Create a new :class:`ConnectionPool` based on host, port and scheme.
+
+ This method is used to actually create the connection pools handed out
+ by :meth:`connection_from_url` and companion methods. It is intended
+ to be overridden for customization.
+ """
+ pool_cls = self.pool_classes_by_scheme[scheme]
+ kwargs = self.connection_pool_kw
+ if scheme == 'http':
+ kwargs = self.connection_pool_kw.copy()
+ for kw in SSL_KEYWORDS:
+ kwargs.pop(kw, None)
+
+ return pool_cls(host, port, **kwargs)
+
+ def clear(self):
+ """
+ Empty our store of pools and direct them all to close.
+
+ This will not affect in-flight connections, but they will not be
+ re-used after completion.
+ """
+ self.pools.clear()
+
+ def connection_from_host(self, host, port=None, scheme='http'):
+ """
+ Get a :class:`ConnectionPool` based on the host, port, and scheme.
+
+ If ``port`` isn't given, it will be derived from the ``scheme`` using
+ ``urllib3.connectionpool.port_by_scheme``.
+ """
+
+ if not host:
+ raise LocationValueError("No host specified.")
+
+ request_context = self.connection_pool_kw.copy()
+ request_context['scheme'] = scheme or 'http'
+ if not port:
+ port = port_by_scheme.get(request_context['scheme'].lower(), 80)
+ request_context['port'] = port
+ request_context['host'] = host
+
+ return self.connection_from_context(request_context)
+
+ def connection_from_context(self, request_context):
+ """
+ Get a :class:`ConnectionPool` based on the request context.
+
+ ``request_context`` must at least contain the ``scheme`` key and its
+ value must be a key in ``key_fn_by_scheme`` instance variable.
+ """
+ scheme = request_context['scheme'].lower()
+ pool_key_constructor = self.key_fn_by_scheme[scheme]
+ pool_key = pool_key_constructor(request_context)
+
+ return self.connection_from_pool_key(pool_key)
+
+ def connection_from_pool_key(self, pool_key):
+ """
+ Get a :class:`ConnectionPool` based on the provided pool key.
+
+ ``pool_key`` should be a namedtuple that only contains immutable
+ objects. At a minimum it must have the ``scheme``, ``host``, and
+ ``port`` fields.
+ """
+ with self.pools.lock:
+ # If the scheme, host, or port doesn't match existing open
+ # connections, open a new ConnectionPool.
+ pool = self.pools.get(pool_key)
+ if pool:
+ return pool
+
+ # Make a fresh ConnectionPool of the desired type
+ pool = self._new_pool(pool_key.scheme, pool_key.host, pool_key.port)
+ self.pools[pool_key] = pool
+
+ return pool
+
+ def connection_from_url(self, url):
+ """
+ Similar to :func:`urllib3.connectionpool.connection_from_url` but
+ doesn't pass any additional parameters to the
+ :class:`urllib3.connectionpool.ConnectionPool` constructor.
+
+ Additional parameters are taken from the :class:`.PoolManager`
+ constructor.
+ """
+ u = parse_url(url)
+ return self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
+
+ def urlopen(self, method, url, redirect=True, **kw):
+ """
+ Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen`
+ with custom cross-host redirect logic and only sends the request-uri
+ portion of the ``url``.
+
+ The given ``url`` parameter must be absolute, such that an appropriate
+ :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it.
+ """
+ u = parse_url(url)
+ conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
+
+ kw['assert_same_host'] = False
+ kw['redirect'] = False
+ if 'headers' not in kw:
+ kw['headers'] = self.headers
+
+ if self.proxy is not None and u.scheme == "http":
+ response = conn.urlopen(method, url, **kw)
+ else:
+ response = conn.urlopen(method, u.request_uri, **kw)
+
+ redirect_location = redirect and response.get_redirect_location()
+ if not redirect_location:
+ return response
+
+ # Support relative URLs for redirecting.
+ redirect_location = urljoin(url, redirect_location)
+
+ # RFC 7231, Section 6.4.4
+ if response.status == 303:
+ method = 'GET'
+
+ retries = kw.get('retries')
+ if not isinstance(retries, Retry):
+ retries = Retry.from_int(retries, redirect=redirect)
+
+ try:
+ retries = retries.increment(method, url, response=response, _pool=conn)
+ except MaxRetryError:
+ if retries.raise_on_redirect:
+ raise
+ return response
+
+ kw['retries'] = retries
+ kw['redirect'] = redirect
+
+ log.info("Redirecting %s -> %s", url, redirect_location)
+ return self.urlopen(method, redirect_location, **kw)
+
+
+class ProxyManager(PoolManager):
+ """
+ Behaves just like :class:`PoolManager`, but sends all requests through
+ the defined proxy, using the CONNECT method for HTTPS URLs.
+
+ :param proxy_url:
+ The URL of the proxy to be used.
+
+ :param proxy_headers:
+ A dictionary contaning headers that will be sent to the proxy. In case
+ of HTTP they are being sent with each request, while in the
+ HTTPS/CONNECT case they are sent only once. Could be used for proxy
+ authentication.
+
+ Example:
+ >>> proxy = urllib3.ProxyManager('http://localhost:3128/')
+ >>> r1 = proxy.request('GET', 'http://google.com/')
+ >>> r2 = proxy.request('GET', 'http://httpbin.org/')
+ >>> len(proxy.pools)
+ 1
+ >>> r3 = proxy.request('GET', 'https://httpbin.org/')
+ >>> r4 = proxy.request('GET', 'https://twitter.com/')
+ >>> len(proxy.pools)
+ 3
+
+ """
+
+ def __init__(self, proxy_url, num_pools=10, headers=None,
+ proxy_headers=None, **connection_pool_kw):
+
+ if isinstance(proxy_url, HTTPConnectionPool):
+ proxy_url = '%s://%s:%i' % (proxy_url.scheme, proxy_url.host,
+ proxy_url.port)
+ proxy = parse_url(proxy_url)
+ if not proxy.port:
+ port = port_by_scheme.get(proxy.scheme, 80)
+ proxy = proxy._replace(port=port)
+
+ if proxy.scheme not in ("http", "https"):
+ raise ProxySchemeUnknown(proxy.scheme)
+
+ self.proxy = proxy
+ self.proxy_headers = proxy_headers or {}
+
+ connection_pool_kw['_proxy'] = self.proxy
+ connection_pool_kw['_proxy_headers'] = self.proxy_headers
+
+ super(ProxyManager, self).__init__(
+ num_pools, headers, **connection_pool_kw)
+
+ def connection_from_host(self, host, port=None, scheme='http'):
+ if scheme == "https":
+ return super(ProxyManager, self).connection_from_host(
+ host, port, scheme)
+
+ return super(ProxyManager, self).connection_from_host(
+ self.proxy.host, self.proxy.port, self.proxy.scheme)
+
+ def _set_proxy_headers(self, url, headers=None):
+ """
+ Sets headers needed by proxies: specifically, the Accept and Host
+ headers. Only sets headers not provided by the user.
+ """
+ headers_ = {'Accept': '*/*'}
+
+ netloc = parse_url(url).netloc
+ if netloc:
+ headers_['Host'] = netloc
+
+ if headers:
+ headers_.update(headers)
+ return headers_
+
+ def urlopen(self, method, url, redirect=True, **kw):
+ "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute."
+ u = parse_url(url)
+
+ if u.scheme == "http":
+ # For proxied HTTPS requests, httplib sets the necessary headers
+ # on the CONNECT to the proxy. For HTTP, we'll definitely
+ # need to set 'Host' at the very least.
+ headers = kw.get('headers', self.headers)
+ kw['headers'] = self._set_proxy_headers(url, headers)
+
+ return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw)
+
+
+def proxy_from_url(url, **kw):
+ return ProxyManager(proxy_url=url, **kw)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/request.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/request.py
index 9d789d6..c0fddff 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/request.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/request.py
@@ -1,148 +1,148 @@
-from __future__ import absolute_import
-
-from .filepost import encode_multipart_formdata
-from .packages.six.moves.urllib.parse import urlencode
-
-
-__all__ = ['RequestMethods']
-
-
-class RequestMethods(object):
- """
- Convenience mixin for classes who implement a :meth:`urlopen` method, such
- as :class:`~urllib3.connectionpool.HTTPConnectionPool` and
- :class:`~urllib3.poolmanager.PoolManager`.
-
- Provides behavior for making common types of HTTP request methods and
- decides which type of request field encoding to use.
-
- Specifically,
-
- :meth:`.request_encode_url` is for sending requests whose fields are
- encoded in the URL (such as GET, HEAD, DELETE).
-
- :meth:`.request_encode_body` is for sending requests whose fields are
- encoded in the *body* of the request using multipart or www-form-urlencoded
- (such as for POST, PUT, PATCH).
-
- :meth:`.request` is for making any kind of request, it will look up the
- appropriate encoding format and use one of the above two methods to make
- the request.
-
- Initializer parameters:
-
- :param headers:
- Headers to include with all requests, unless other headers are given
- explicitly.
- """
-
- _encode_url_methods = set(['DELETE', 'GET', 'HEAD', 'OPTIONS'])
-
- def __init__(self, headers=None):
- self.headers = headers or {}
-
- def urlopen(self, method, url, body=None, headers=None,
- encode_multipart=True, multipart_boundary=None,
- **kw): # Abstract
- raise NotImplemented("Classes extending RequestMethods must implement "
- "their own ``urlopen`` method.")
-
- def request(self, method, url, fields=None, headers=None, **urlopen_kw):
- """
- Make a request using :meth:`urlopen` with the appropriate encoding of
- ``fields`` based on the ``method`` used.
-
- This is a convenience method that requires the least amount of manual
- effort. It can be used in most situations, while still having the
- option to drop down to more specific methods when necessary, such as
- :meth:`request_encode_url`, :meth:`request_encode_body`,
- or even the lowest level :meth:`urlopen`.
- """
- method = method.upper()
-
- if method in self._encode_url_methods:
- return self.request_encode_url(method, url, fields=fields,
- headers=headers,
- **urlopen_kw)
- else:
- return self.request_encode_body(method, url, fields=fields,
- headers=headers,
- **urlopen_kw)
-
- def request_encode_url(self, method, url, fields=None, headers=None,
- **urlopen_kw):
- """
- Make a request using :meth:`urlopen` with the ``fields`` encoded in
- the url. This is useful for request methods like GET, HEAD, DELETE, etc.
- """
- if headers is None:
- headers = self.headers
-
- extra_kw = {'headers': headers}
- extra_kw.update(urlopen_kw)
-
- if fields:
- url += '?' + urlencode(fields)
-
- return self.urlopen(method, url, **extra_kw)
-
- def request_encode_body(self, method, url, fields=None, headers=None,
- encode_multipart=True, multipart_boundary=None,
- **urlopen_kw):
- """
- Make a request using :meth:`urlopen` with the ``fields`` encoded in
- the body. This is useful for request methods like POST, PUT, PATCH, etc.
-
- When ``encode_multipart=True`` (default), then
- :meth:`urllib3.filepost.encode_multipart_formdata` is used to encode
- the payload with the appropriate content type. Otherwise
- :meth:`urllib.urlencode` is used with the
- 'application/x-www-form-urlencoded' content type.
-
- Multipart encoding must be used when posting files, and it's reasonably
- safe to use it in other times too. However, it may break request
- signing, such as with OAuth.
-
- Supports an optional ``fields`` parameter of key/value strings AND
- key/filetuple. A filetuple is a (filename, data, MIME type) tuple where
- the MIME type is optional. For example::
-
- fields = {
- 'foo': 'bar',
- 'fakefile': ('foofile.txt', 'contents of foofile'),
- 'realfile': ('barfile.txt', open('realfile').read()),
- 'typedfile': ('bazfile.bin', open('bazfile').read(),
- 'image/jpeg'),
- 'nonamefile': 'contents of nonamefile field',
- }
-
- When uploading a file, providing a filename (the first parameter of the
- tuple) is optional but recommended to best mimick behavior of browsers.
-
- Note that if ``headers`` are supplied, the 'Content-Type' header will
- be overwritten because it depends on the dynamic random boundary string
- which is used to compose the body of the request. The random boundary
- string can be explicitly set with the ``multipart_boundary`` parameter.
- """
- if headers is None:
- headers = self.headers
-
- extra_kw = {'headers': {}}
-
- if fields:
- if 'body' in urlopen_kw:
- raise TypeError(
- "request got values for both 'fields' and 'body', can only specify one.")
-
- if encode_multipart:
- body, content_type = encode_multipart_formdata(fields, boundary=multipart_boundary)
- else:
- body, content_type = urlencode(fields), 'application/x-www-form-urlencoded'
-
- extra_kw['body'] = body
- extra_kw['headers'] = {'Content-Type': content_type}
-
- extra_kw['headers'].update(headers)
- extra_kw.update(urlopen_kw)
-
- return self.urlopen(method, url, **extra_kw)
+from __future__ import absolute_import
+
+from .filepost import encode_multipart_formdata
+from .packages.six.moves.urllib.parse import urlencode
+
+
+__all__ = ['RequestMethods']
+
+
+class RequestMethods(object):
+ """
+ Convenience mixin for classes who implement a :meth:`urlopen` method, such
+ as :class:`~urllib3.connectionpool.HTTPConnectionPool` and
+ :class:`~urllib3.poolmanager.PoolManager`.
+
+ Provides behavior for making common types of HTTP request methods and
+ decides which type of request field encoding to use.
+
+ Specifically,
+
+ :meth:`.request_encode_url` is for sending requests whose fields are
+ encoded in the URL (such as GET, HEAD, DELETE).
+
+ :meth:`.request_encode_body` is for sending requests whose fields are
+ encoded in the *body* of the request using multipart or www-form-urlencoded
+ (such as for POST, PUT, PATCH).
+
+ :meth:`.request` is for making any kind of request, it will look up the
+ appropriate encoding format and use one of the above two methods to make
+ the request.
+
+ Initializer parameters:
+
+ :param headers:
+ Headers to include with all requests, unless other headers are given
+ explicitly.
+ """
+
+ _encode_url_methods = set(['DELETE', 'GET', 'HEAD', 'OPTIONS'])
+
+ def __init__(self, headers=None):
+ self.headers = headers or {}
+
+ def urlopen(self, method, url, body=None, headers=None,
+ encode_multipart=True, multipart_boundary=None,
+ **kw): # Abstract
+ raise NotImplemented("Classes extending RequestMethods must implement "
+ "their own ``urlopen`` method.")
+
+ def request(self, method, url, fields=None, headers=None, **urlopen_kw):
+ """
+ Make a request using :meth:`urlopen` with the appropriate encoding of
+ ``fields`` based on the ``method`` used.
+
+ This is a convenience method that requires the least amount of manual
+ effort. It can be used in most situations, while still having the
+ option to drop down to more specific methods when necessary, such as
+ :meth:`request_encode_url`, :meth:`request_encode_body`,
+ or even the lowest level :meth:`urlopen`.
+ """
+ method = method.upper()
+
+ if method in self._encode_url_methods:
+ return self.request_encode_url(method, url, fields=fields,
+ headers=headers,
+ **urlopen_kw)
+ else:
+ return self.request_encode_body(method, url, fields=fields,
+ headers=headers,
+ **urlopen_kw)
+
+ def request_encode_url(self, method, url, fields=None, headers=None,
+ **urlopen_kw):
+ """
+ Make a request using :meth:`urlopen` with the ``fields`` encoded in
+ the url. This is useful for request methods like GET, HEAD, DELETE, etc.
+ """
+ if headers is None:
+ headers = self.headers
+
+ extra_kw = {'headers': headers}
+ extra_kw.update(urlopen_kw)
+
+ if fields:
+ url += '?' + urlencode(fields)
+
+ return self.urlopen(method, url, **extra_kw)
+
+ def request_encode_body(self, method, url, fields=None, headers=None,
+ encode_multipart=True, multipart_boundary=None,
+ **urlopen_kw):
+ """
+ Make a request using :meth:`urlopen` with the ``fields`` encoded in
+ the body. This is useful for request methods like POST, PUT, PATCH, etc.
+
+ When ``encode_multipart=True`` (default), then
+ :meth:`urllib3.filepost.encode_multipart_formdata` is used to encode
+ the payload with the appropriate content type. Otherwise
+ :meth:`urllib.urlencode` is used with the
+ 'application/x-www-form-urlencoded' content type.
+
+ Multipart encoding must be used when posting files, and it's reasonably
+ safe to use it in other times too. However, it may break request
+ signing, such as with OAuth.
+
+ Supports an optional ``fields`` parameter of key/value strings AND
+ key/filetuple. A filetuple is a (filename, data, MIME type) tuple where
+ the MIME type is optional. For example::
+
+ fields = {
+ 'foo': 'bar',
+ 'fakefile': ('foofile.txt', 'contents of foofile'),
+ 'realfile': ('barfile.txt', open('realfile').read()),
+ 'typedfile': ('bazfile.bin', open('bazfile').read(),
+ 'image/jpeg'),
+ 'nonamefile': 'contents of nonamefile field',
+ }
+
+ When uploading a file, providing a filename (the first parameter of the
+ tuple) is optional but recommended to best mimick behavior of browsers.
+
+ Note that if ``headers`` are supplied, the 'Content-Type' header will
+ be overwritten because it depends on the dynamic random boundary string
+ which is used to compose the body of the request. The random boundary
+ string can be explicitly set with the ``multipart_boundary`` parameter.
+ """
+ if headers is None:
+ headers = self.headers
+
+ extra_kw = {'headers': {}}
+
+ if fields:
+ if 'body' in urlopen_kw:
+ raise TypeError(
+ "request got values for both 'fields' and 'body', can only specify one.")
+
+ if encode_multipart:
+ body, content_type = encode_multipart_formdata(fields, boundary=multipart_boundary)
+ else:
+ body, content_type = urlencode(fields), 'application/x-www-form-urlencoded'
+
+ extra_kw['body'] = body
+ extra_kw['headers'] = {'Content-Type': content_type}
+
+ extra_kw['headers'].update(headers)
+ extra_kw.update(urlopen_kw)
+
+ return self.urlopen(method, url, **extra_kw)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/response.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/response.py
index 55db29a..6f1b63c 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/response.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/response.py
@@ -1,618 +1,618 @@
-from __future__ import absolute_import
-from contextlib import contextmanager
-import zlib
-import io
-import logging
-from socket import timeout as SocketTimeout
-from socket import error as SocketError
-
-from ._collections import HTTPHeaderDict
-from .exceptions import (
- BodyNotHttplibCompatible, ProtocolError, DecodeError, ReadTimeoutError,
- ResponseNotChunked, IncompleteRead, InvalidHeader
-)
-from .packages.six import string_types as basestring, binary_type, PY3
-from .packages.six.moves import http_client as httplib
-from .connection import HTTPException, BaseSSLError
-from .util.response import is_fp_closed, is_response_to_head
-
-log = logging.getLogger(__name__)
-
-
-class DeflateDecoder(object):
-
- def __init__(self):
- self._first_try = True
- self._data = binary_type()
- self._obj = zlib.decompressobj()
-
- def __getattr__(self, name):
- return getattr(self._obj, name)
-
- def decompress(self, data):
- if not data:
- return data
-
- if not self._first_try:
- return self._obj.decompress(data)
-
- self._data += data
- try:
- return self._obj.decompress(data)
- except zlib.error:
- self._first_try = False
- self._obj = zlib.decompressobj(-zlib.MAX_WBITS)
- try:
- return self.decompress(self._data)
- finally:
- self._data = None
-
-
-class GzipDecoder(object):
-
- def __init__(self):
- self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
-
- def __getattr__(self, name):
- return getattr(self._obj, name)
-
- def decompress(self, data):
- if not data:
- return data
- return self._obj.decompress(data)
-
-
-def _get_decoder(mode):
- if mode == 'gzip':
- return GzipDecoder()
-
- return DeflateDecoder()
-
-
-class HTTPResponse(io.IOBase):
- """
- HTTP Response container.
-
- Backwards-compatible to httplib's HTTPResponse but the response ``body`` is
- loaded and decoded on-demand when the ``data`` property is accessed. This
- class is also compatible with the Python standard library's :mod:`io`
- module, and can hence be treated as a readable object in the context of that
- framework.
-
- Extra parameters for behaviour not present in httplib.HTTPResponse:
-
- :param preload_content:
- If True, the response's body will be preloaded during construction.
-
- :param decode_content:
- If True, attempts to decode specific content-encoding's based on headers
- (like 'gzip' and 'deflate') will be skipped and raw data will be used
- instead.
-
- :param original_response:
- When this HTTPResponse wrapper is generated from an httplib.HTTPResponse
- object, it's convenient to include the original for debug purposes. It's
- otherwise unused.
-
- :param retries:
- The retries contains the last :class:`~urllib3.util.retry.Retry` that
- was used during the request.
-
- :param enforce_content_length:
- Enforce content length checking. Body returned by server must match
- value of Content-Length header, if present. Otherwise, raise error.
- """
-
- CONTENT_DECODERS = ['gzip', 'deflate']
- REDIRECT_STATUSES = [301, 302, 303, 307, 308]
-
- def __init__(self, body='', headers=None, status=0, version=0, reason=None,
- strict=0, preload_content=True, decode_content=True,
- original_response=None, pool=None, connection=None,
- retries=None, enforce_content_length=False, request_method=None):
-
- if isinstance(headers, HTTPHeaderDict):
- self.headers = headers
- else:
- self.headers = HTTPHeaderDict(headers)
- self.status = status
- self.version = version
- self.reason = reason
- self.strict = strict
- self.decode_content = decode_content
- self.retries = retries
- self.enforce_content_length = enforce_content_length
-
- self._decoder = None
- self._body = None
- self._fp = None
- self._original_response = original_response
- self._fp_bytes_read = 0
-
- if body and isinstance(body, (basestring, binary_type)):
- self._body = body
-
- self._pool = pool
- self._connection = connection
-
- if hasattr(body, 'read'):
- self._fp = body
-
- # Are we using the chunked-style of transfer encoding?
- self.chunked = False
- self.chunk_left = None
- tr_enc = self.headers.get('transfer-encoding', '').lower()
- # Don't incur the penalty of creating a list and then discarding it
- encodings = (enc.strip() for enc in tr_enc.split(","))
- if "chunked" in encodings:
- self.chunked = True
-
- # Determine length of response
- self.length_remaining = self._init_length(request_method)
-
- # If requested, preload the body.
- if preload_content and not self._body:
- self._body = self.read(decode_content=decode_content)
-
- def get_redirect_location(self):
- """
- Should we redirect and where to?
-
- :returns: Truthy redirect location string if we got a redirect status
- code and valid location. ``None`` if redirect status and no
- location. ``False`` if not a redirect status code.
- """
- if self.status in self.REDIRECT_STATUSES:
- return self.headers.get('location')
-
- return False
-
- def release_conn(self):
- if not self._pool or not self._connection:
- return
-
- self._pool._put_conn(self._connection)
- self._connection = None
-
- @property
- def data(self):
- # For backwords-compat with earlier urllib3 0.4 and earlier.
- if self._body:
- return self._body
-
- if self._fp:
- return self.read(cache_content=True)
-
- @property
- def connection(self):
- return self._connection
-
- def tell(self):
- """
- Obtain the number of bytes pulled over the wire so far. May differ from
- the amount of content returned by :meth:``HTTPResponse.read`` if bytes
- are encoded on the wire (e.g, compressed).
- """
- return self._fp_bytes_read
-
- def _init_length(self, request_method):
- """
- Set initial length value for Response content if available.
- """
- length = self.headers.get('content-length')
-
- if length is not None and self.chunked:
- # This Response will fail with an IncompleteRead if it can't be
- # received as chunked. This method falls back to attempt reading
- # the response before raising an exception.
- log.warning("Received response with both Content-Length and "
- "Transfer-Encoding set. This is expressly forbidden "
- "by RFC 7230 sec 3.3.2. Ignoring Content-Length and "
- "attempting to process response as Transfer-Encoding: "
- "chunked.")
- return None
-
- elif length is not None:
- try:
- # RFC 7230 section 3.3.2 specifies multiple content lengths can
- # be sent in a single Content-Length header
- # (e.g. Content-Length: 42, 42). This line ensures the values
- # are all valid ints and that as long as the `set` length is 1,
- # all values are the same. Otherwise, the header is invalid.
- lengths = set([int(val) for val in length.split(',')])
- if len(lengths) > 1:
- raise InvalidHeader("Content-Length contained multiple "
- "unmatching values (%s)" % length)
- length = lengths.pop()
- except ValueError:
- length = None
- else:
- if length < 0:
- length = None
-
- # Convert status to int for comparison
- # In some cases, httplib returns a status of "_UNKNOWN"
- try:
- status = int(self.status)
- except ValueError:
- status = 0
-
- # Check for responses that shouldn't include a body
- if status in (204, 304) or 100 <= status < 200 or request_method == 'HEAD':
- length = 0
-
- return length
-
- def _init_decoder(self):
- """
- Set-up the _decoder attribute if necessary.
- """
- # Note: content-encoding value should be case-insensitive, per RFC 7230
- # Section 3.2
- content_encoding = self.headers.get('content-encoding', '').lower()
- if self._decoder is None and content_encoding in self.CONTENT_DECODERS:
- self._decoder = _get_decoder(content_encoding)
-
- def _decode(self, data, decode_content, flush_decoder):
- """
- Decode the data passed in and potentially flush the decoder.
- """
- try:
- if decode_content and self._decoder:
- data = self._decoder.decompress(data)
- except (IOError, zlib.error) as e:
- content_encoding = self.headers.get('content-encoding', '').lower()
- raise DecodeError(
- "Received response with content-encoding: %s, but "
- "failed to decode it." % content_encoding, e)
-
- if flush_decoder and decode_content:
- data += self._flush_decoder()
-
- return data
-
- def _flush_decoder(self):
- """
- Flushes the decoder. Should only be called if the decoder is actually
- being used.
- """
- if self._decoder:
- buf = self._decoder.decompress(b'')
- return buf + self._decoder.flush()
-
- return b''
-
- @contextmanager
- def _error_catcher(self):
- """
- Catch low-level python exceptions, instead re-raising urllib3
- variants, so that low-level exceptions are not leaked in the
- high-level api.
-
- On exit, release the connection back to the pool.
- """
- clean_exit = False
-
- try:
- try:
- yield
-
- except SocketTimeout:
- # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but
- # there is yet no clean way to get at it from this context.
- raise ReadTimeoutError(self._pool, None, 'Read timed out.')
-
- except BaseSSLError as e:
- # FIXME: Is there a better way to differentiate between SSLErrors?
- if 'read operation timed out' not in str(e): # Defensive:
- # This shouldn't happen but just in case we're missing an edge
- # case, let's avoid swallowing SSL errors.
- raise
-
- raise ReadTimeoutError(self._pool, None, 'Read timed out.')
-
- except (HTTPException, SocketError) as e:
- # This includes IncompleteRead.
- raise ProtocolError('Connection broken: %r' % e, e)
-
- # If no exception is thrown, we should avoid cleaning up
- # unnecessarily.
- clean_exit = True
- finally:
- # If we didn't terminate cleanly, we need to throw away our
- # connection.
- if not clean_exit:
- # The response may not be closed but we're not going to use it
- # anymore so close it now to ensure that the connection is
- # released back to the pool.
- if self._original_response:
- self._original_response.close()
-
- # Closing the response may not actually be sufficient to close
- # everything, so if we have a hold of the connection close that
- # too.
- if self._connection:
- self._connection.close()
-
- # If we hold the original response but it's closed now, we should
- # return the connection back to the pool.
- if self._original_response and self._original_response.isclosed():
- self.release_conn()
-
- def read(self, amt=None, decode_content=None, cache_content=False):
- """
- Similar to :meth:`httplib.HTTPResponse.read`, but with two additional
- parameters: ``decode_content`` and ``cache_content``.
-
- :param amt:
- How much of the content to read. If specified, caching is skipped
- because it doesn't make sense to cache partial content as the full
- response.
-
- :param decode_content:
- If True, will attempt to decode the body based on the
- 'content-encoding' header.
-
- :param cache_content:
- If True, will save the returned data such that the same result is
- returned despite of the state of the underlying file object. This
- is useful if you want the ``.data`` property to continue working
- after having ``.read()`` the file object. (Overridden if ``amt`` is
- set.)
- """
- self._init_decoder()
- if decode_content is None:
- decode_content = self.decode_content
-
- if self._fp is None:
- return
-
- flush_decoder = False
- data = None
-
- with self._error_catcher():
- if amt is None:
- # cStringIO doesn't like amt=None
- data = self._fp.read()
- flush_decoder = True
- else:
- cache_content = False
- data = self._fp.read(amt)
- if amt != 0 and not data: # Platform-specific: Buggy versions of Python.
- # Close the connection when no data is returned
- #
- # This is redundant to what httplib/http.client _should_
- # already do. However, versions of python released before
- # December 15, 2012 (http://bugs.python.org/issue16298) do
- # not properly close the connection in all cases. There is
- # no harm in redundantly calling close.
- self._fp.close()
- flush_decoder = True
- if self.enforce_content_length and self.length_remaining not in (0, None):
- # This is an edge case that httplib failed to cover due
- # to concerns of backward compatibility. We're
- # addressing it here to make sure IncompleteRead is
- # raised during streaming, so all calls with incorrect
- # Content-Length are caught.
- raise IncompleteRead(self._fp_bytes_read, self.length_remaining)
-
- if data:
- self._fp_bytes_read += len(data)
- if self.length_remaining is not None:
- self.length_remaining -= len(data)
-
- data = self._decode(data, decode_content, flush_decoder)
-
- if cache_content:
- self._body = data
-
- return data
-
- def stream(self, amt=2**16, decode_content=None):
- """
- A generator wrapper for the read() method. A call will block until
- ``amt`` bytes have been read from the connection or until the
- connection is closed.
-
- :param amt:
- How much of the content to read. The generator will return up to
- much data per iteration, but may return less. This is particularly
- likely when using compressed data. However, the empty string will
- never be returned.
-
- :param decode_content:
- If True, will attempt to decode the body based on the
- 'content-encoding' header.
- """
- if self.chunked and self.supports_chunked_reads():
- for line in self.read_chunked(amt, decode_content=decode_content):
- yield line
- else:
- while not is_fp_closed(self._fp):
- data = self.read(amt=amt, decode_content=decode_content)
-
- if data:
- yield data
-
- @classmethod
- def from_httplib(ResponseCls, r, **response_kw):
- """
- Given an :class:`httplib.HTTPResponse` instance ``r``, return a
- corresponding :class:`urllib3.response.HTTPResponse` object.
-
- Remaining parameters are passed to the HTTPResponse constructor, along
- with ``original_response=r``.
- """
- headers = r.msg
-
- if not isinstance(headers, HTTPHeaderDict):
- if PY3: # Python 3
- headers = HTTPHeaderDict(headers.items())
- else: # Python 2
- headers = HTTPHeaderDict.from_httplib(headers)
-
- # HTTPResponse objects in Python 3 don't have a .strict attribute
- strict = getattr(r, 'strict', 0)
- resp = ResponseCls(body=r,
- headers=headers,
- status=r.status,
- version=r.version,
- reason=r.reason,
- strict=strict,
- original_response=r,
- **response_kw)
- return resp
-
- # Backwards-compatibility methods for httplib.HTTPResponse
- def getheaders(self):
- return self.headers
-
- def getheader(self, name, default=None):
- return self.headers.get(name, default)
-
- # Overrides from io.IOBase
- def close(self):
- if not self.closed:
- self._fp.close()
-
- if self._connection:
- self._connection.close()
-
- @property
- def closed(self):
- if self._fp is None:
- return True
- elif hasattr(self._fp, 'isclosed'):
- return self._fp.isclosed()
- elif hasattr(self._fp, 'closed'):
- return self._fp.closed
- else:
- return True
-
- def fileno(self):
- if self._fp is None:
- raise IOError("HTTPResponse has no file to get a fileno from")
- elif hasattr(self._fp, "fileno"):
- return self._fp.fileno()
- else:
- raise IOError("The file-like object this HTTPResponse is wrapped "
- "around has no file descriptor")
-
- def flush(self):
- if self._fp is not None and hasattr(self._fp, 'flush'):
- return self._fp.flush()
-
- def readable(self):
- # This method is required for `io` module compatibility.
- return True
-
- def readinto(self, b):
- # This method is required for `io` module compatibility.
- temp = self.read(len(b))
- if len(temp) == 0:
- return 0
- else:
- b[:len(temp)] = temp
- return len(temp)
-
- def supports_chunked_reads(self):
- """
- Checks if the underlying file-like object looks like a
- httplib.HTTPResponse object. We do this by testing for the fp
- attribute. If it is present we assume it returns raw chunks as
- processed by read_chunked().
- """
- return hasattr(self._fp, 'fp')
-
- def _update_chunk_length(self):
- # First, we'll figure out length of a chunk and then
- # we'll try to read it from socket.
- if self.chunk_left is not None:
- return
- line = self._fp.fp.readline()
- line = line.split(b';', 1)[0]
- try:
- self.chunk_left = int(line, 16)
- except ValueError:
- # Invalid chunked protocol response, abort.
- self.close()
- raise httplib.IncompleteRead(line)
-
- def _handle_chunk(self, amt):
- returned_chunk = None
- if amt is None:
- chunk = self._fp._safe_read(self.chunk_left)
- returned_chunk = chunk
- self._fp._safe_read(2) # Toss the CRLF at the end of the chunk.
- self.chunk_left = None
- elif amt < self.chunk_left:
- value = self._fp._safe_read(amt)
- self.chunk_left = self.chunk_left - amt
- returned_chunk = value
- elif amt == self.chunk_left:
- value = self._fp._safe_read(amt)
- self._fp._safe_read(2) # Toss the CRLF at the end of the chunk.
- self.chunk_left = None
- returned_chunk = value
- else: # amt > self.chunk_left
- returned_chunk = self._fp._safe_read(self.chunk_left)
- self._fp._safe_read(2) # Toss the CRLF at the end of the chunk.
- self.chunk_left = None
- return returned_chunk
-
- def read_chunked(self, amt=None, decode_content=None):
- """
- Similar to :meth:`HTTPResponse.read`, but with an additional
- parameter: ``decode_content``.
-
- :param decode_content:
- If True, will attempt to decode the body based on the
- 'content-encoding' header.
- """
- self._init_decoder()
- # FIXME: Rewrite this method and make it a class with a better structured logic.
- if not self.chunked:
- raise ResponseNotChunked(
- "Response is not chunked. "
- "Header 'transfer-encoding: chunked' is missing.")
- if not self.supports_chunked_reads():
- raise BodyNotHttplibCompatible(
- "Body should be httplib.HTTPResponse like. "
- "It should have have an fp attribute which returns raw chunks.")
-
- # Don't bother reading the body of a HEAD request.
- if self._original_response and is_response_to_head(self._original_response):
- self._original_response.close()
- return
-
- with self._error_catcher():
- while True:
- self._update_chunk_length()
- if self.chunk_left == 0:
- break
- chunk = self._handle_chunk(amt)
- decoded = self._decode(chunk, decode_content=decode_content,
- flush_decoder=False)
- if decoded:
- yield decoded
-
- if decode_content:
- # On CPython and PyPy, we should never need to flush the
- # decoder. However, on Jython we *might* need to, so
- # lets defensively do it anyway.
- decoded = self._flush_decoder()
- if decoded: # Platform-specific: Jython.
- yield decoded
-
- # Chunk content ends with \r\n: discard it.
- while True:
- line = self._fp.fp.readline()
- if not line:
- # Some sites may not end with '\r\n'.
- break
- if line == b'\r\n':
- break
-
- # We read everything; close the "file".
- if self._original_response:
- self._original_response.close()
+from __future__ import absolute_import
+from contextlib import contextmanager
+import zlib
+import io
+import logging
+from socket import timeout as SocketTimeout
+from socket import error as SocketError
+
+from ._collections import HTTPHeaderDict
+from .exceptions import (
+ BodyNotHttplibCompatible, ProtocolError, DecodeError, ReadTimeoutError,
+ ResponseNotChunked, IncompleteRead, InvalidHeader
+)
+from .packages.six import string_types as basestring, binary_type, PY3
+from .packages.six.moves import http_client as httplib
+from .connection import HTTPException, BaseSSLError
+from .util.response import is_fp_closed, is_response_to_head
+
+log = logging.getLogger(__name__)
+
+
+class DeflateDecoder(object):
+
+ def __init__(self):
+ self._first_try = True
+ self._data = binary_type()
+ self._obj = zlib.decompressobj()
+
+ def __getattr__(self, name):
+ return getattr(self._obj, name)
+
+ def decompress(self, data):
+ if not data:
+ return data
+
+ if not self._first_try:
+ return self._obj.decompress(data)
+
+ self._data += data
+ try:
+ return self._obj.decompress(data)
+ except zlib.error:
+ self._first_try = False
+ self._obj = zlib.decompressobj(-zlib.MAX_WBITS)
+ try:
+ return self.decompress(self._data)
+ finally:
+ self._data = None
+
+
+class GzipDecoder(object):
+
+ def __init__(self):
+ self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
+
+ def __getattr__(self, name):
+ return getattr(self._obj, name)
+
+ def decompress(self, data):
+ if not data:
+ return data
+ return self._obj.decompress(data)
+
+
+def _get_decoder(mode):
+ if mode == 'gzip':
+ return GzipDecoder()
+
+ return DeflateDecoder()
+
+
+class HTTPResponse(io.IOBase):
+ """
+ HTTP Response container.
+
+ Backwards-compatible to httplib's HTTPResponse but the response ``body`` is
+ loaded and decoded on-demand when the ``data`` property is accessed. This
+ class is also compatible with the Python standard library's :mod:`io`
+ module, and can hence be treated as a readable object in the context of that
+ framework.
+
+ Extra parameters for behaviour not present in httplib.HTTPResponse:
+
+ :param preload_content:
+ If True, the response's body will be preloaded during construction.
+
+ :param decode_content:
+ If True, attempts to decode specific content-encoding's based on headers
+ (like 'gzip' and 'deflate') will be skipped and raw data will be used
+ instead.
+
+ :param original_response:
+ When this HTTPResponse wrapper is generated from an httplib.HTTPResponse
+ object, it's convenient to include the original for debug purposes. It's
+ otherwise unused.
+
+ :param retries:
+ The retries contains the last :class:`~urllib3.util.retry.Retry` that
+ was used during the request.
+
+ :param enforce_content_length:
+ Enforce content length checking. Body returned by server must match
+ value of Content-Length header, if present. Otherwise, raise error.
+ """
+
+ CONTENT_DECODERS = ['gzip', 'deflate']
+ REDIRECT_STATUSES = [301, 302, 303, 307, 308]
+
+ def __init__(self, body='', headers=None, status=0, version=0, reason=None,
+ strict=0, preload_content=True, decode_content=True,
+ original_response=None, pool=None, connection=None,
+ retries=None, enforce_content_length=False, request_method=None):
+
+ if isinstance(headers, HTTPHeaderDict):
+ self.headers = headers
+ else:
+ self.headers = HTTPHeaderDict(headers)
+ self.status = status
+ self.version = version
+ self.reason = reason
+ self.strict = strict
+ self.decode_content = decode_content
+ self.retries = retries
+ self.enforce_content_length = enforce_content_length
+
+ self._decoder = None
+ self._body = None
+ self._fp = None
+ self._original_response = original_response
+ self._fp_bytes_read = 0
+
+ if body and isinstance(body, (basestring, binary_type)):
+ self._body = body
+
+ self._pool = pool
+ self._connection = connection
+
+ if hasattr(body, 'read'):
+ self._fp = body
+
+ # Are we using the chunked-style of transfer encoding?
+ self.chunked = False
+ self.chunk_left = None
+ tr_enc = self.headers.get('transfer-encoding', '').lower()
+ # Don't incur the penalty of creating a list and then discarding it
+ encodings = (enc.strip() for enc in tr_enc.split(","))
+ if "chunked" in encodings:
+ self.chunked = True
+
+ # Determine length of response
+ self.length_remaining = self._init_length(request_method)
+
+ # If requested, preload the body.
+ if preload_content and not self._body:
+ self._body = self.read(decode_content=decode_content)
+
+ def get_redirect_location(self):
+ """
+ Should we redirect and where to?
+
+ :returns: Truthy redirect location string if we got a redirect status
+ code and valid location. ``None`` if redirect status and no
+ location. ``False`` if not a redirect status code.
+ """
+ if self.status in self.REDIRECT_STATUSES:
+ return self.headers.get('location')
+
+ return False
+
+ def release_conn(self):
+ if not self._pool or not self._connection:
+ return
+
+ self._pool._put_conn(self._connection)
+ self._connection = None
+
+ @property
+ def data(self):
+ # For backwords-compat with earlier urllib3 0.4 and earlier.
+ if self._body:
+ return self._body
+
+ if self._fp:
+ return self.read(cache_content=True)
+
+ @property
+ def connection(self):
+ return self._connection
+
+ def tell(self):
+ """
+ Obtain the number of bytes pulled over the wire so far. May differ from
+ the amount of content returned by :meth:``HTTPResponse.read`` if bytes
+ are encoded on the wire (e.g, compressed).
+ """
+ return self._fp_bytes_read
+
+ def _init_length(self, request_method):
+ """
+ Set initial length value for Response content if available.
+ """
+ length = self.headers.get('content-length')
+
+ if length is not None and self.chunked:
+ # This Response will fail with an IncompleteRead if it can't be
+ # received as chunked. This method falls back to attempt reading
+ # the response before raising an exception.
+ log.warning("Received response with both Content-Length and "
+ "Transfer-Encoding set. This is expressly forbidden "
+ "by RFC 7230 sec 3.3.2. Ignoring Content-Length and "
+ "attempting to process response as Transfer-Encoding: "
+ "chunked.")
+ return None
+
+ elif length is not None:
+ try:
+ # RFC 7230 section 3.3.2 specifies multiple content lengths can
+ # be sent in a single Content-Length header
+ # (e.g. Content-Length: 42, 42). This line ensures the values
+ # are all valid ints and that as long as the `set` length is 1,
+ # all values are the same. Otherwise, the header is invalid.
+ lengths = set([int(val) for val in length.split(',')])
+ if len(lengths) > 1:
+ raise InvalidHeader("Content-Length contained multiple "
+ "unmatching values (%s)" % length)
+ length = lengths.pop()
+ except ValueError:
+ length = None
+ else:
+ if length < 0:
+ length = None
+
+ # Convert status to int for comparison
+ # In some cases, httplib returns a status of "_UNKNOWN"
+ try:
+ status = int(self.status)
+ except ValueError:
+ status = 0
+
+ # Check for responses that shouldn't include a body
+ if status in (204, 304) or 100 <= status < 200 or request_method == 'HEAD':
+ length = 0
+
+ return length
+
+ def _init_decoder(self):
+ """
+ Set-up the _decoder attribute if necessary.
+ """
+ # Note: content-encoding value should be case-insensitive, per RFC 7230
+ # Section 3.2
+ content_encoding = self.headers.get('content-encoding', '').lower()
+ if self._decoder is None and content_encoding in self.CONTENT_DECODERS:
+ self._decoder = _get_decoder(content_encoding)
+
+ def _decode(self, data, decode_content, flush_decoder):
+ """
+ Decode the data passed in and potentially flush the decoder.
+ """
+ try:
+ if decode_content and self._decoder:
+ data = self._decoder.decompress(data)
+ except (IOError, zlib.error) as e:
+ content_encoding = self.headers.get('content-encoding', '').lower()
+ raise DecodeError(
+ "Received response with content-encoding: %s, but "
+ "failed to decode it." % content_encoding, e)
+
+ if flush_decoder and decode_content:
+ data += self._flush_decoder()
+
+ return data
+
+ def _flush_decoder(self):
+ """
+ Flushes the decoder. Should only be called if the decoder is actually
+ being used.
+ """
+ if self._decoder:
+ buf = self._decoder.decompress(b'')
+ return buf + self._decoder.flush()
+
+ return b''
+
+ @contextmanager
+ def _error_catcher(self):
+ """
+ Catch low-level python exceptions, instead re-raising urllib3
+ variants, so that low-level exceptions are not leaked in the
+ high-level api.
+
+ On exit, release the connection back to the pool.
+ """
+ clean_exit = False
+
+ try:
+ try:
+ yield
+
+ except SocketTimeout:
+ # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but
+ # there is yet no clean way to get at it from this context.
+ raise ReadTimeoutError(self._pool, None, 'Read timed out.')
+
+ except BaseSSLError as e:
+ # FIXME: Is there a better way to differentiate between SSLErrors?
+ if 'read operation timed out' not in str(e): # Defensive:
+ # This shouldn't happen but just in case we're missing an edge
+ # case, let's avoid swallowing SSL errors.
+ raise
+
+ raise ReadTimeoutError(self._pool, None, 'Read timed out.')
+
+ except (HTTPException, SocketError) as e:
+ # This includes IncompleteRead.
+ raise ProtocolError('Connection broken: %r' % e, e)
+
+ # If no exception is thrown, we should avoid cleaning up
+ # unnecessarily.
+ clean_exit = True
+ finally:
+ # If we didn't terminate cleanly, we need to throw away our
+ # connection.
+ if not clean_exit:
+ # The response may not be closed but we're not going to use it
+ # anymore so close it now to ensure that the connection is
+ # released back to the pool.
+ if self._original_response:
+ self._original_response.close()
+
+ # Closing the response may not actually be sufficient to close
+ # everything, so if we have a hold of the connection close that
+ # too.
+ if self._connection:
+ self._connection.close()
+
+ # If we hold the original response but it's closed now, we should
+ # return the connection back to the pool.
+ if self._original_response and self._original_response.isclosed():
+ self.release_conn()
+
+ def read(self, amt=None, decode_content=None, cache_content=False):
+ """
+ Similar to :meth:`httplib.HTTPResponse.read`, but with two additional
+ parameters: ``decode_content`` and ``cache_content``.
+
+ :param amt:
+ How much of the content to read. If specified, caching is skipped
+ because it doesn't make sense to cache partial content as the full
+ response.
+
+ :param decode_content:
+ If True, will attempt to decode the body based on the
+ 'content-encoding' header.
+
+ :param cache_content:
+ If True, will save the returned data such that the same result is
+ returned despite of the state of the underlying file object. This
+ is useful if you want the ``.data`` property to continue working
+ after having ``.read()`` the file object. (Overridden if ``amt`` is
+ set.)
+ """
+ self._init_decoder()
+ if decode_content is None:
+ decode_content = self.decode_content
+
+ if self._fp is None:
+ return
+
+ flush_decoder = False
+ data = None
+
+ with self._error_catcher():
+ if amt is None:
+ # cStringIO doesn't like amt=None
+ data = self._fp.read()
+ flush_decoder = True
+ else:
+ cache_content = False
+ data = self._fp.read(amt)
+ if amt != 0 and not data: # Platform-specific: Buggy versions of Python.
+ # Close the connection when no data is returned
+ #
+ # This is redundant to what httplib/http.client _should_
+ # already do. However, versions of python released before
+ # December 15, 2012 (http://bugs.python.org/issue16298) do
+ # not properly close the connection in all cases. There is
+ # no harm in redundantly calling close.
+ self._fp.close()
+ flush_decoder = True
+ if self.enforce_content_length and self.length_remaining not in (0, None):
+ # This is an edge case that httplib failed to cover due
+ # to concerns of backward compatibility. We're
+ # addressing it here to make sure IncompleteRead is
+ # raised during streaming, so all calls with incorrect
+ # Content-Length are caught.
+ raise IncompleteRead(self._fp_bytes_read, self.length_remaining)
+
+ if data:
+ self._fp_bytes_read += len(data)
+ if self.length_remaining is not None:
+ self.length_remaining -= len(data)
+
+ data = self._decode(data, decode_content, flush_decoder)
+
+ if cache_content:
+ self._body = data
+
+ return data
+
+ def stream(self, amt=2**16, decode_content=None):
+ """
+ A generator wrapper for the read() method. A call will block until
+ ``amt`` bytes have been read from the connection or until the
+ connection is closed.
+
+ :param amt:
+ How much of the content to read. The generator will return up to
+ much data per iteration, but may return less. This is particularly
+ likely when using compressed data. However, the empty string will
+ never be returned.
+
+ :param decode_content:
+ If True, will attempt to decode the body based on the
+ 'content-encoding' header.
+ """
+ if self.chunked and self.supports_chunked_reads():
+ for line in self.read_chunked(amt, decode_content=decode_content):
+ yield line
+ else:
+ while not is_fp_closed(self._fp):
+ data = self.read(amt=amt, decode_content=decode_content)
+
+ if data:
+ yield data
+
+ @classmethod
+ def from_httplib(ResponseCls, r, **response_kw):
+ """
+ Given an :class:`httplib.HTTPResponse` instance ``r``, return a
+ corresponding :class:`urllib3.response.HTTPResponse` object.
+
+ Remaining parameters are passed to the HTTPResponse constructor, along
+ with ``original_response=r``.
+ """
+ headers = r.msg
+
+ if not isinstance(headers, HTTPHeaderDict):
+ if PY3: # Python 3
+ headers = HTTPHeaderDict(headers.items())
+ else: # Python 2
+ headers = HTTPHeaderDict.from_httplib(headers)
+
+ # HTTPResponse objects in Python 3 don't have a .strict attribute
+ strict = getattr(r, 'strict', 0)
+ resp = ResponseCls(body=r,
+ headers=headers,
+ status=r.status,
+ version=r.version,
+ reason=r.reason,
+ strict=strict,
+ original_response=r,
+ **response_kw)
+ return resp
+
+ # Backwards-compatibility methods for httplib.HTTPResponse
+ def getheaders(self):
+ return self.headers
+
+ def getheader(self, name, default=None):
+ return self.headers.get(name, default)
+
+ # Overrides from io.IOBase
+ def close(self):
+ if not self.closed:
+ self._fp.close()
+
+ if self._connection:
+ self._connection.close()
+
+ @property
+ def closed(self):
+ if self._fp is None:
+ return True
+ elif hasattr(self._fp, 'isclosed'):
+ return self._fp.isclosed()
+ elif hasattr(self._fp, 'closed'):
+ return self._fp.closed
+ else:
+ return True
+
+ def fileno(self):
+ if self._fp is None:
+ raise IOError("HTTPResponse has no file to get a fileno from")
+ elif hasattr(self._fp, "fileno"):
+ return self._fp.fileno()
+ else:
+ raise IOError("The file-like object this HTTPResponse is wrapped "
+ "around has no file descriptor")
+
+ def flush(self):
+ if self._fp is not None and hasattr(self._fp, 'flush'):
+ return self._fp.flush()
+
+ def readable(self):
+ # This method is required for `io` module compatibility.
+ return True
+
+ def readinto(self, b):
+ # This method is required for `io` module compatibility.
+ temp = self.read(len(b))
+ if len(temp) == 0:
+ return 0
+ else:
+ b[:len(temp)] = temp
+ return len(temp)
+
+ def supports_chunked_reads(self):
+ """
+ Checks if the underlying file-like object looks like a
+ httplib.HTTPResponse object. We do this by testing for the fp
+ attribute. If it is present we assume it returns raw chunks as
+ processed by read_chunked().
+ """
+ return hasattr(self._fp, 'fp')
+
+ def _update_chunk_length(self):
+ # First, we'll figure out length of a chunk and then
+ # we'll try to read it from socket.
+ if self.chunk_left is not None:
+ return
+ line = self._fp.fp.readline()
+ line = line.split(b';', 1)[0]
+ try:
+ self.chunk_left = int(line, 16)
+ except ValueError:
+ # Invalid chunked protocol response, abort.
+ self.close()
+ raise httplib.IncompleteRead(line)
+
+ def _handle_chunk(self, amt):
+ returned_chunk = None
+ if amt is None:
+ chunk = self._fp._safe_read(self.chunk_left)
+ returned_chunk = chunk
+ self._fp._safe_read(2) # Toss the CRLF at the end of the chunk.
+ self.chunk_left = None
+ elif amt < self.chunk_left:
+ value = self._fp._safe_read(amt)
+ self.chunk_left = self.chunk_left - amt
+ returned_chunk = value
+ elif amt == self.chunk_left:
+ value = self._fp._safe_read(amt)
+ self._fp._safe_read(2) # Toss the CRLF at the end of the chunk.
+ self.chunk_left = None
+ returned_chunk = value
+ else: # amt > self.chunk_left
+ returned_chunk = self._fp._safe_read(self.chunk_left)
+ self._fp._safe_read(2) # Toss the CRLF at the end of the chunk.
+ self.chunk_left = None
+ return returned_chunk
+
+ def read_chunked(self, amt=None, decode_content=None):
+ """
+ Similar to :meth:`HTTPResponse.read`, but with an additional
+ parameter: ``decode_content``.
+
+ :param decode_content:
+ If True, will attempt to decode the body based on the
+ 'content-encoding' header.
+ """
+ self._init_decoder()
+ # FIXME: Rewrite this method and make it a class with a better structured logic.
+ if not self.chunked:
+ raise ResponseNotChunked(
+ "Response is not chunked. "
+ "Header 'transfer-encoding: chunked' is missing.")
+ if not self.supports_chunked_reads():
+ raise BodyNotHttplibCompatible(
+ "Body should be httplib.HTTPResponse like. "
+ "It should have have an fp attribute which returns raw chunks.")
+
+ # Don't bother reading the body of a HEAD request.
+ if self._original_response and is_response_to_head(self._original_response):
+ self._original_response.close()
+ return
+
+ with self._error_catcher():
+ while True:
+ self._update_chunk_length()
+ if self.chunk_left == 0:
+ break
+ chunk = self._handle_chunk(amt)
+ decoded = self._decode(chunk, decode_content=decode_content,
+ flush_decoder=False)
+ if decoded:
+ yield decoded
+
+ if decode_content:
+ # On CPython and PyPy, we should never need to flush the
+ # decoder. However, on Jython we *might* need to, so
+ # lets defensively do it anyway.
+ decoded = self._flush_decoder()
+ if decoded: # Platform-specific: Jython.
+ yield decoded
+
+ # Chunk content ends with \r\n: discard it.
+ while True:
+ line = self._fp.fp.readline()
+ if not line:
+ # Some sites may not end with '\r\n'.
+ break
+ if line == b'\r\n':
+ break
+
+ # We read everything; close the "file".
+ if self._original_response:
+ self._original_response.close()
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/__init__.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/__init__.py
index ab66169..5ced5a4 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/__init__.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/__init__.py
@@ -1,52 +1,52 @@
-from __future__ import absolute_import
-# For backwards compatibility, provide imports that used to be here.
-from .connection import is_connection_dropped
-from .request import make_headers
-from .response import is_fp_closed
-from .ssl_ import (
- SSLContext,
- HAS_SNI,
- IS_PYOPENSSL,
- assert_fingerprint,
- resolve_cert_reqs,
- resolve_ssl_version,
- ssl_wrap_socket,
-)
-from .timeout import (
- current_time,
- Timeout,
-)
-
-from .retry import Retry
-from .url import (
- get_host,
- parse_url,
- split_first,
- Url,
-)
-from .wait import (
- wait_for_read,
- wait_for_write
-)
-
-__all__ = (
- 'HAS_SNI',
- 'IS_PYOPENSSL',
- 'SSLContext',
- 'Retry',
- 'Timeout',
- 'Url',
- 'assert_fingerprint',
- 'current_time',
- 'is_connection_dropped',
- 'is_fp_closed',
- 'get_host',
- 'parse_url',
- 'make_headers',
- 'resolve_cert_reqs',
- 'resolve_ssl_version',
- 'split_first',
- 'ssl_wrap_socket',
- 'wait_for_read',
- 'wait_for_write'
-)
+from __future__ import absolute_import
+# For backwards compatibility, provide imports that used to be here.
+from .connection import is_connection_dropped
+from .request import make_headers
+from .response import is_fp_closed
+from .ssl_ import (
+ SSLContext,
+ HAS_SNI,
+ IS_PYOPENSSL,
+ assert_fingerprint,
+ resolve_cert_reqs,
+ resolve_ssl_version,
+ ssl_wrap_socket,
+)
+from .timeout import (
+ current_time,
+ Timeout,
+)
+
+from .retry import Retry
+from .url import (
+ get_host,
+ parse_url,
+ split_first,
+ Url,
+)
+from .wait import (
+ wait_for_read,
+ wait_for_write
+)
+
+__all__ = (
+ 'HAS_SNI',
+ 'IS_PYOPENSSL',
+ 'SSLContext',
+ 'Retry',
+ 'Timeout',
+ 'Url',
+ 'assert_fingerprint',
+ 'current_time',
+ 'is_connection_dropped',
+ 'is_fp_closed',
+ 'get_host',
+ 'parse_url',
+ 'make_headers',
+ 'resolve_cert_reqs',
+ 'resolve_ssl_version',
+ 'split_first',
+ 'ssl_wrap_socket',
+ 'wait_for_read',
+ 'wait_for_write'
+)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/connection.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/connection.py
index 31ecd83..bf699cf 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/connection.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/connection.py
@@ -1,130 +1,130 @@
-from __future__ import absolute_import
-import socket
-from .wait import wait_for_read
-from .selectors import HAS_SELECT, SelectorError
-
-
-def is_connection_dropped(conn): # Platform-specific
- """
- Returns True if the connection is dropped and should be closed.
-
- :param conn:
- :class:`httplib.HTTPConnection` object.
-
- Note: For platforms like AppEngine, this will always return ``False`` to
- let the platform handle connection recycling transparently for us.
- """
- sock = getattr(conn, 'sock', False)
- if sock is False: # Platform-specific: AppEngine
- return False
- if sock is None: # Connection already closed (such as by httplib).
- return True
-
- if not HAS_SELECT:
- return False
-
- try:
- return bool(wait_for_read(sock, timeout=0.0))
- except SelectorError:
- return True
-
-
-# This function is copied from socket.py in the Python 2.7 standard
-# library test suite. Added to its signature is only `socket_options`.
-# One additional modification is that we avoid binding to IPv6 servers
-# discovered in DNS if the system doesn't have IPv6 functionality.
-def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
- source_address=None, socket_options=None):
- """Connect to *address* and return the socket object.
-
- Convenience function. Connect to *address* (a 2-tuple ``(host,
- port)``) and return the socket object. Passing the optional
- *timeout* parameter will set the timeout on the socket instance
- before attempting to connect. If no *timeout* is supplied, the
- global default timeout setting returned by :func:`getdefaulttimeout`
- is used. If *source_address* is set it must be a tuple of (host, port)
- for the socket to bind as a source address before making the connection.
- An host of '' or port 0 tells the OS to use the default.
- """
-
- host, port = address
- if host.startswith('['):
- host = host.strip('[]')
- err = None
-
- # Using the value from allowed_gai_family() in the context of getaddrinfo lets
- # us select whether to work with IPv4 DNS records, IPv6 records, or both.
- # The original create_connection function always returns all records.
- family = allowed_gai_family()
-
- for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
- af, socktype, proto, canonname, sa = res
- sock = None
- try:
- sock = socket.socket(af, socktype, proto)
-
- # If provided, set socket level options before connecting.
- _set_socket_options(sock, socket_options)
-
- if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
- sock.settimeout(timeout)
- if source_address:
- sock.bind(source_address)
- sock.connect(sa)
- return sock
-
- except socket.error as e:
- err = e
- if sock is not None:
- sock.close()
- sock = None
-
- if err is not None:
- raise err
-
- raise socket.error("getaddrinfo returns an empty list")
-
-
-def _set_socket_options(sock, options):
- if options is None:
- return
-
- for opt in options:
- sock.setsockopt(*opt)
-
-
-def allowed_gai_family():
- """This function is designed to work in the context of
- getaddrinfo, where family=socket.AF_UNSPEC is the default and
- will perform a DNS search for both IPv6 and IPv4 records."""
-
- family = socket.AF_INET
- if HAS_IPV6:
- family = socket.AF_UNSPEC
- return family
-
-
-def _has_ipv6(host):
- """ Returns True if the system can bind an IPv6 address. """
- sock = None
- has_ipv6 = False
-
- if socket.has_ipv6:
- # has_ipv6 returns true if cPython was compiled with IPv6 support.
- # It does not tell us if the system has IPv6 support enabled. To
- # determine that we must bind to an IPv6 address.
- # https://github.com/shazow/urllib3/pull/611
- # https://bugs.python.org/issue658327
- try:
- sock = socket.socket(socket.AF_INET6)
- sock.bind((host, 0))
- has_ipv6 = True
- except Exception:
- pass
-
- if sock:
- sock.close()
- return has_ipv6
-
-
-HAS_IPV6 = _has_ipv6('::1')
+from __future__ import absolute_import
+import socket
+from .wait import wait_for_read
+from .selectors import HAS_SELECT, SelectorError
+
+
+def is_connection_dropped(conn): # Platform-specific
+ """
+ Returns True if the connection is dropped and should be closed.
+
+ :param conn:
+ :class:`httplib.HTTPConnection` object.
+
+ Note: For platforms like AppEngine, this will always return ``False`` to
+ let the platform handle connection recycling transparently for us.
+ """
+ sock = getattr(conn, 'sock', False)
+ if sock is False: # Platform-specific: AppEngine
+ return False
+ if sock is None: # Connection already closed (such as by httplib).
+ return True
+
+ if not HAS_SELECT:
+ return False
+
+ try:
+ return bool(wait_for_read(sock, timeout=0.0))
+ except SelectorError:
+ return True
+
+
+# This function is copied from socket.py in the Python 2.7 standard
+# library test suite. Added to its signature is only `socket_options`.
+# One additional modification is that we avoid binding to IPv6 servers
+# discovered in DNS if the system doesn't have IPv6 functionality.
+def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
+ source_address=None, socket_options=None):
+ """Connect to *address* and return the socket object.
+
+ Convenience function. Connect to *address* (a 2-tuple ``(host,
+ port)``) and return the socket object. Passing the optional
+ *timeout* parameter will set the timeout on the socket instance
+ before attempting to connect. If no *timeout* is supplied, the
+ global default timeout setting returned by :func:`getdefaulttimeout`
+ is used. If *source_address* is set it must be a tuple of (host, port)
+ for the socket to bind as a source address before making the connection.
+ An host of '' or port 0 tells the OS to use the default.
+ """
+
+ host, port = address
+ if host.startswith('['):
+ host = host.strip('[]')
+ err = None
+
+ # Using the value from allowed_gai_family() in the context of getaddrinfo lets
+ # us select whether to work with IPv4 DNS records, IPv6 records, or both.
+ # The original create_connection function always returns all records.
+ family = allowed_gai_family()
+
+ for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
+ af, socktype, proto, canonname, sa = res
+ sock = None
+ try:
+ sock = socket.socket(af, socktype, proto)
+
+ # If provided, set socket level options before connecting.
+ _set_socket_options(sock, socket_options)
+
+ if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
+ sock.settimeout(timeout)
+ if source_address:
+ sock.bind(source_address)
+ sock.connect(sa)
+ return sock
+
+ except socket.error as e:
+ err = e
+ if sock is not None:
+ sock.close()
+ sock = None
+
+ if err is not None:
+ raise err
+
+ raise socket.error("getaddrinfo returns an empty list")
+
+
+def _set_socket_options(sock, options):
+ if options is None:
+ return
+
+ for opt in options:
+ sock.setsockopt(*opt)
+
+
+def allowed_gai_family():
+ """This function is designed to work in the context of
+ getaddrinfo, where family=socket.AF_UNSPEC is the default and
+ will perform a DNS search for both IPv6 and IPv4 records."""
+
+ family = socket.AF_INET
+ if HAS_IPV6:
+ family = socket.AF_UNSPEC
+ return family
+
+
+def _has_ipv6(host):
+ """ Returns True if the system can bind an IPv6 address. """
+ sock = None
+ has_ipv6 = False
+
+ if socket.has_ipv6:
+ # has_ipv6 returns true if cPython was compiled with IPv6 support.
+ # It does not tell us if the system has IPv6 support enabled. To
+ # determine that we must bind to an IPv6 address.
+ # https://github.com/shazow/urllib3/pull/611
+ # https://bugs.python.org/issue658327
+ try:
+ sock = socket.socket(socket.AF_INET6)
+ sock.bind((host, 0))
+ has_ipv6 = True
+ except Exception:
+ pass
+
+ if sock:
+ sock.close()
+ return has_ipv6
+
+
+HAS_IPV6 = _has_ipv6('::1')
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/request.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/request.py
index 31a3d44..974fc40 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/request.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/request.py
@@ -1,118 +1,118 @@
-from __future__ import absolute_import
-from base64 import b64encode
-
-from ..packages.six import b, integer_types
-from ..exceptions import UnrewindableBodyError
-
-ACCEPT_ENCODING = 'gzip,deflate'
-_FAILEDTELL = object()
-
-
-def make_headers(keep_alive=None, accept_encoding=None, user_agent=None,
- basic_auth=None, proxy_basic_auth=None, disable_cache=None):
- """
- Shortcuts for generating request headers.
-
- :param keep_alive:
- If ``True``, adds 'connection: keep-alive' header.
-
- :param accept_encoding:
- Can be a boolean, list, or string.
- ``True`` translates to 'gzip,deflate'.
- List will get joined by comma.
- String will be used as provided.
-
- :param user_agent:
- String representing the user-agent you want, such as
- "python-urllib3/0.6"
-
- :param basic_auth:
- Colon-separated username:password string for 'authorization: basic ...'
- auth header.
-
- :param proxy_basic_auth:
- Colon-separated username:password string for 'proxy-authorization: basic ...'
- auth header.
-
- :param disable_cache:
- If ``True``, adds 'cache-control: no-cache' header.
-
- Example::
-
- >>> make_headers(keep_alive=True, user_agent="Batman/1.0")
- {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'}
- >>> make_headers(accept_encoding=True)
- {'accept-encoding': 'gzip,deflate'}
- """
- headers = {}
- if accept_encoding:
- if isinstance(accept_encoding, str):
- pass
- elif isinstance(accept_encoding, list):
- accept_encoding = ','.join(accept_encoding)
- else:
- accept_encoding = ACCEPT_ENCODING
- headers['accept-encoding'] = accept_encoding
-
- if user_agent:
- headers['user-agent'] = user_agent
-
- if keep_alive:
- headers['connection'] = 'keep-alive'
-
- if basic_auth:
- headers['authorization'] = 'Basic ' + \
- b64encode(b(basic_auth)).decode('utf-8')
-
- if proxy_basic_auth:
- headers['proxy-authorization'] = 'Basic ' + \
- b64encode(b(proxy_basic_auth)).decode('utf-8')
-
- if disable_cache:
- headers['cache-control'] = 'no-cache'
-
- return headers
-
-
-def set_file_position(body, pos):
- """
- If a position is provided, move file to that point.
- Otherwise, we'll attempt to record a position for future use.
- """
- if pos is not None:
- rewind_body(body, pos)
- elif getattr(body, 'tell', None) is not None:
- try:
- pos = body.tell()
- except (IOError, OSError):
- # This differentiates from None, allowing us to catch
- # a failed `tell()` later when trying to rewind the body.
- pos = _FAILEDTELL
-
- return pos
-
-
-def rewind_body(body, body_pos):
- """
- Attempt to rewind body to a certain position.
- Primarily used for request redirects and retries.
-
- :param body:
- File-like object that supports seek.
-
- :param int pos:
- Position to seek to in file.
- """
- body_seek = getattr(body, 'seek', None)
- if body_seek is not None and isinstance(body_pos, integer_types):
- try:
- body_seek(body_pos)
- except (IOError, OSError):
- raise UnrewindableBodyError("An error occured when rewinding request "
- "body for redirect/retry.")
- elif body_pos is _FAILEDTELL:
- raise UnrewindableBodyError("Unable to record file position for rewinding "
- "request body during a redirect/retry.")
- else:
- raise ValueError("body_pos must be of type integer, "
- "instead it was %s." % type(body_pos))
+from __future__ import absolute_import
+from base64 import b64encode
+
+from ..packages.six import b, integer_types
+from ..exceptions import UnrewindableBodyError
+
+ACCEPT_ENCODING = 'gzip,deflate'
+_FAILEDTELL = object()
+
+
+def make_headers(keep_alive=None, accept_encoding=None, user_agent=None,
+ basic_auth=None, proxy_basic_auth=None, disable_cache=None):
+ """
+ Shortcuts for generating request headers.
+
+ :param keep_alive:
+ If ``True``, adds 'connection: keep-alive' header.
+
+ :param accept_encoding:
+ Can be a boolean, list, or string.
+ ``True`` translates to 'gzip,deflate'.
+ List will get joined by comma.
+ String will be used as provided.
+
+ :param user_agent:
+ String representing the user-agent you want, such as
+ "python-urllib3/0.6"
+
+ :param basic_auth:
+ Colon-separated username:password string for 'authorization: basic ...'
+ auth header.
+
+ :param proxy_basic_auth:
+ Colon-separated username:password string for 'proxy-authorization: basic ...'
+ auth header.
+
+ :param disable_cache:
+ If ``True``, adds 'cache-control: no-cache' header.
+
+ Example::
+
+ >>> make_headers(keep_alive=True, user_agent="Batman/1.0")
+ {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'}
+ >>> make_headers(accept_encoding=True)
+ {'accept-encoding': 'gzip,deflate'}
+ """
+ headers = {}
+ if accept_encoding:
+ if isinstance(accept_encoding, str):
+ pass
+ elif isinstance(accept_encoding, list):
+ accept_encoding = ','.join(accept_encoding)
+ else:
+ accept_encoding = ACCEPT_ENCODING
+ headers['accept-encoding'] = accept_encoding
+
+ if user_agent:
+ headers['user-agent'] = user_agent
+
+ if keep_alive:
+ headers['connection'] = 'keep-alive'
+
+ if basic_auth:
+ headers['authorization'] = 'Basic ' + \
+ b64encode(b(basic_auth)).decode('utf-8')
+
+ if proxy_basic_auth:
+ headers['proxy-authorization'] = 'Basic ' + \
+ b64encode(b(proxy_basic_auth)).decode('utf-8')
+
+ if disable_cache:
+ headers['cache-control'] = 'no-cache'
+
+ return headers
+
+
+def set_file_position(body, pos):
+ """
+ If a position is provided, move file to that point.
+ Otherwise, we'll attempt to record a position for future use.
+ """
+ if pos is not None:
+ rewind_body(body, pos)
+ elif getattr(body, 'tell', None) is not None:
+ try:
+ pos = body.tell()
+ except (IOError, OSError):
+ # This differentiates from None, allowing us to catch
+ # a failed `tell()` later when trying to rewind the body.
+ pos = _FAILEDTELL
+
+ return pos
+
+
+def rewind_body(body, body_pos):
+ """
+ Attempt to rewind body to a certain position.
+ Primarily used for request redirects and retries.
+
+ :param body:
+ File-like object that supports seek.
+
+ :param int pos:
+ Position to seek to in file.
+ """
+ body_seek = getattr(body, 'seek', None)
+ if body_seek is not None and isinstance(body_pos, integer_types):
+ try:
+ body_seek(body_pos)
+ except (IOError, OSError):
+ raise UnrewindableBodyError("An error occured when rewinding request "
+ "body for redirect/retry.")
+ elif body_pos is _FAILEDTELL:
+ raise UnrewindableBodyError("Unable to record file position for rewinding "
+ "request body during a redirect/retry.")
+ else:
+ raise ValueError("body_pos must be of type integer, "
+ "instead it was %s." % type(body_pos))
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/response.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/response.py
index c2eb49c..67cf730 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/response.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/response.py
@@ -1,81 +1,81 @@
-from __future__ import absolute_import
-from ..packages.six.moves import http_client as httplib
-
-from ..exceptions import HeaderParsingError
-
-
-def is_fp_closed(obj):
- """
- Checks whether a given file-like object is closed.
-
- :param obj:
- The file-like object to check.
- """
-
- try:
- # Check `isclosed()` first, in case Python3 doesn't set `closed`.
- # GH Issue #928
- return obj.isclosed()
- except AttributeError:
- pass
-
- try:
- # Check via the official file-like-object way.
- return obj.closed
- except AttributeError:
- pass
-
- try:
- # Check if the object is a container for another file-like object that
- # gets released on exhaustion (e.g. HTTPResponse).
- return obj.fp is None
- except AttributeError:
- pass
-
- raise ValueError("Unable to determine whether fp is closed.")
-
-
-def assert_header_parsing(headers):
- """
- Asserts whether all headers have been successfully parsed.
- Extracts encountered errors from the result of parsing headers.
-
- Only works on Python 3.
-
- :param headers: Headers to verify.
- :type headers: `httplib.HTTPMessage`.
-
- :raises urllib3.exceptions.HeaderParsingError:
- If parsing errors are found.
- """
-
- # This will fail silently if we pass in the wrong kind of parameter.
- # To make debugging easier add an explicit check.
- if not isinstance(headers, httplib.HTTPMessage):
- raise TypeError('expected httplib.Message, got {0}.'.format(
- type(headers)))
-
- defects = getattr(headers, 'defects', None)
- get_payload = getattr(headers, 'get_payload', None)
-
- unparsed_data = None
- if get_payload: # Platform-specific: Python 3.
- unparsed_data = get_payload()
-
- if defects or unparsed_data:
- raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data)
-
-
-def is_response_to_head(response):
- """
- Checks whether the request of a response has been a HEAD-request.
- Handles the quirks of AppEngine.
-
- :param conn:
- :type conn: :class:`httplib.HTTPResponse`
- """
- # FIXME: Can we do this somehow without accessing private httplib _method?
- method = response._method
- if isinstance(method, int): # Platform-specific: Appengine
- return method == 3
- return method.upper() == 'HEAD'
+from __future__ import absolute_import
+from ..packages.six.moves import http_client as httplib
+
+from ..exceptions import HeaderParsingError
+
+
+def is_fp_closed(obj):
+ """
+ Checks whether a given file-like object is closed.
+
+ :param obj:
+ The file-like object to check.
+ """
+
+ try:
+ # Check `isclosed()` first, in case Python3 doesn't set `closed`.
+ # GH Issue #928
+ return obj.isclosed()
+ except AttributeError:
+ pass
+
+ try:
+ # Check via the official file-like-object way.
+ return obj.closed
+ except AttributeError:
+ pass
+
+ try:
+ # Check if the object is a container for another file-like object that
+ # gets released on exhaustion (e.g. HTTPResponse).
+ return obj.fp is None
+ except AttributeError:
+ pass
+
+ raise ValueError("Unable to determine whether fp is closed.")
+
+
+def assert_header_parsing(headers):
+ """
+ Asserts whether all headers have been successfully parsed.
+ Extracts encountered errors from the result of parsing headers.
+
+ Only works on Python 3.
+
+ :param headers: Headers to verify.
+ :type headers: `httplib.HTTPMessage`.
+
+ :raises urllib3.exceptions.HeaderParsingError:
+ If parsing errors are found.
+ """
+
+ # This will fail silently if we pass in the wrong kind of parameter.
+ # To make debugging easier add an explicit check.
+ if not isinstance(headers, httplib.HTTPMessage):
+ raise TypeError('expected httplib.Message, got {0}.'.format(
+ type(headers)))
+
+ defects = getattr(headers, 'defects', None)
+ get_payload = getattr(headers, 'get_payload', None)
+
+ unparsed_data = None
+ if get_payload: # Platform-specific: Python 3.
+ unparsed_data = get_payload()
+
+ if defects or unparsed_data:
+ raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data)
+
+
+def is_response_to_head(response):
+ """
+ Checks whether the request of a response has been a HEAD-request.
+ Handles the quirks of AppEngine.
+
+ :param conn:
+ :type conn: :class:`httplib.HTTPResponse`
+ """
+ # FIXME: Can we do this somehow without accessing private httplib _method?
+ method = response._method
+ if isinstance(method, int): # Platform-specific: Appengine
+ return method == 3
+ return method.upper() == 'HEAD'
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/retry.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/retry.py
index 9258eca..c9e7d28 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/retry.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/retry.py
@@ -1,389 +1,389 @@
-from __future__ import absolute_import
-import time
-import logging
-from collections import namedtuple
-from itertools import takewhile
-import email
-import re
-
-from ..exceptions import (
- ConnectTimeoutError,
- MaxRetryError,
- ProtocolError,
- ReadTimeoutError,
- ResponseError,
- InvalidHeader,
-)
-from ..packages import six
-
-
-log = logging.getLogger(__name__)
-
-# Data structure for representing the metadata of requests that result in a retry.
-RequestHistory = namedtuple('RequestHistory', ["method", "url", "error",
- "status", "redirect_location"])
-
-
-class Retry(object):
- """ Retry configuration.
-
- Each retry attempt will create a new Retry object with updated values, so
- they can be safely reused.
-
- Retries can be defined as a default for a pool::
-
- retries = Retry(connect=5, read=2, redirect=5)
- http = PoolManager(retries=retries)
- response = http.request('GET', 'http://example.com/')
-
- Or per-request (which overrides the default for the pool)::
-
- response = http.request('GET', 'http://example.com/', retries=Retry(10))
-
- Retries can be disabled by passing ``False``::
-
- response = http.request('GET', 'http://example.com/', retries=False)
-
- Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless
- retries are disabled, in which case the causing exception will be raised.
-
- :param int total:
- Total number of retries to allow. Takes precedence over other counts.
-
- Set to ``None`` to remove this constraint and fall back on other
- counts. It's a good idea to set this to some sensibly-high value to
- account for unexpected edge cases and avoid infinite retry loops.
-
- Set to ``0`` to fail on the first retry.
-
- Set to ``False`` to disable and imply ``raise_on_redirect=False``.
-
- :param int connect:
- How many connection-related errors to retry on.
-
- These are errors raised before the request is sent to the remote server,
- which we assume has not triggered the server to process the request.
-
- Set to ``0`` to fail on the first retry of this type.
-
- :param int read:
- How many times to retry on read errors.
-
- These errors are raised after the request was sent to the server, so the
- request may have side-effects.
-
- Set to ``0`` to fail on the first retry of this type.
-
- :param int redirect:
- How many redirects to perform. Limit this to avoid infinite redirect
- loops.
-
- A redirect is a HTTP response with a status code 301, 302, 303, 307 or
- 308.
-
- Set to ``0`` to fail on the first retry of this type.
-
- Set to ``False`` to disable and imply ``raise_on_redirect=False``.
-
- :param iterable method_whitelist:
- Set of uppercased HTTP method verbs that we should retry on.
-
- By default, we only retry on methods which are considered to be
- idempotent (multiple requests with the same parameters end with the
- same state). See :attr:`Retry.DEFAULT_METHOD_WHITELIST`.
-
- Set to a ``False`` value to retry on any verb.
-
- :param iterable status_forcelist:
- A set of integer HTTP status codes that we should force a retry on.
- A retry is initiated if the request method is in ``method_whitelist``
- and the response status code is in ``status_forcelist``.
-
- By default, this is disabled with ``None``.
-
- :param float backoff_factor:
- A backoff factor to apply between attempts after the second try
- (most errors are resolved immediately by a second try without a
- delay). urllib3 will sleep for::
-
- {backoff factor} * (2 ^ ({number of total retries} - 1))
-
- seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep
- for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer
- than :attr:`Retry.BACKOFF_MAX`.
-
- By default, backoff is disabled (set to 0).
-
- :param bool raise_on_redirect: Whether, if the number of redirects is
- exhausted, to raise a MaxRetryError, or to return a response with a
- response code in the 3xx range.
-
- :param bool raise_on_status: Similar meaning to ``raise_on_redirect``:
- whether we should raise an exception, or return a response,
- if status falls in ``status_forcelist`` range and retries have
- been exhausted.
-
- :param tuple history: The history of the request encountered during
- each call to :meth:`~Retry.increment`. The list is in the order
- the requests occurred. Each list item is of class :class:`RequestHistory`.
-
- :param bool respect_retry_after_header:
- Whether to respect Retry-After header on status codes defined as
- :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not.
-
- """
-
- DEFAULT_METHOD_WHITELIST = frozenset([
- 'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'])
-
- RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])
-
- #: Maximum backoff time.
- BACKOFF_MAX = 120
-
- def __init__(self, total=10, connect=None, read=None, redirect=None,
- method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None,
- backoff_factor=0, raise_on_redirect=True, raise_on_status=True,
- history=None, respect_retry_after_header=True):
-
- self.total = total
- self.connect = connect
- self.read = read
-
- if redirect is False or total is False:
- redirect = 0
- raise_on_redirect = False
-
- self.redirect = redirect
- self.status_forcelist = status_forcelist or set()
- self.method_whitelist = method_whitelist
- self.backoff_factor = backoff_factor
- self.raise_on_redirect = raise_on_redirect
- self.raise_on_status = raise_on_status
- self.history = history or tuple()
- self.respect_retry_after_header = respect_retry_after_header
-
- def new(self, **kw):
- params = dict(
- total=self.total,
- connect=self.connect, read=self.read, redirect=self.redirect,
- method_whitelist=self.method_whitelist,
- status_forcelist=self.status_forcelist,
- backoff_factor=self.backoff_factor,
- raise_on_redirect=self.raise_on_redirect,
- raise_on_status=self.raise_on_status,
- history=self.history,
- )
- params.update(kw)
- return type(self)(**params)
-
- @classmethod
- def from_int(cls, retries, redirect=True, default=None):
- """ Backwards-compatibility for the old retries format."""
- if retries is None:
- retries = default if default is not None else cls.DEFAULT
-
- if isinstance(retries, Retry):
- return retries
-
- redirect = bool(redirect) and None
- new_retries = cls(retries, redirect=redirect)
- log.debug("Converted retries value: %r -> %r", retries, new_retries)
- return new_retries
-
- def get_backoff_time(self):
- """ Formula for computing the current backoff
-
- :rtype: float
- """
- # We want to consider only the last consecutive errors sequence (Ignore redirects).
- consecutive_errors_len = len(list(takewhile(lambda x: x.redirect_location is None,
- reversed(self.history))))
- if consecutive_errors_len <= 1:
- return 0
-
- backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1))
- return min(self.BACKOFF_MAX, backoff_value)
-
- def parse_retry_after(self, retry_after):
- # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4
- if re.match(r"^\s*[0-9]+\s*$", retry_after):
- seconds = int(retry_after)
- else:
- retry_date_tuple = email.utils.parsedate(retry_after)
- if retry_date_tuple is None:
- raise InvalidHeader("Invalid Retry-After header: %s" % retry_after)
- retry_date = time.mktime(retry_date_tuple)
- seconds = retry_date - time.time()
-
- if seconds < 0:
- seconds = 0
-
- return seconds
-
- def get_retry_after(self, response):
- """ Get the value of Retry-After in seconds. """
-
- retry_after = response.getheader("Retry-After")
-
- if retry_after is None:
- return None
-
- return self.parse_retry_after(retry_after)
-
- def sleep_for_retry(self, response=None):
- retry_after = self.get_retry_after(response)
- if retry_after:
- time.sleep(retry_after)
- return True
-
- return False
-
- def _sleep_backoff(self):
- backoff = self.get_backoff_time()
- if backoff <= 0:
- return
- time.sleep(backoff)
-
- def sleep(self, response=None):
- """ Sleep between retry attempts.
-
- This method will respect a server's ``Retry-After`` response header
- and sleep the duration of the time requested. If that is not present, it
- will use an exponential backoff. By default, the backoff factor is 0 and
- this method will return immediately.
- """
-
- if response:
- slept = self.sleep_for_retry(response)
- if slept:
- return
-
- self._sleep_backoff()
-
- def _is_connection_error(self, err):
- """ Errors when we're fairly sure that the server did not receive the
- request, so it should be safe to retry.
- """
- return isinstance(err, ConnectTimeoutError)
-
- def _is_read_error(self, err):
- """ Errors that occur after the request has been started, so we should
- assume that the server began processing it.
- """
- return isinstance(err, (ReadTimeoutError, ProtocolError))
-
- def _is_method_retryable(self, method):
- """ Checks if a given HTTP method should be retried upon, depending if
- it is included on the method whitelist.
- """
- if self.method_whitelist and method.upper() not in self.method_whitelist:
- return False
-
- return True
-
- def is_retry(self, method, status_code, has_retry_after=False):
- """ Is this method/status code retryable? (Based on whitelists and control
- variables such as the number of total retries to allow, whether to
- respect the Retry-After header, whether this header is present, and
- whether the returned status code is on the list of status codes to
- be retried upon on the presence of the aforementioned header)
- """
- if not self._is_method_retryable(method):
- return False
-
- if self.status_forcelist and status_code in self.status_forcelist:
- return True
-
- return (self.total and self.respect_retry_after_header and
- has_retry_after and (status_code in self.RETRY_AFTER_STATUS_CODES))
-
- def is_exhausted(self):
- """ Are we out of retries? """
- retry_counts = (self.total, self.connect, self.read, self.redirect)
- retry_counts = list(filter(None, retry_counts))
- if not retry_counts:
- return False
-
- return min(retry_counts) < 0
-
- def increment(self, method=None, url=None, response=None, error=None,
- _pool=None, _stacktrace=None):
- """ Return a new Retry object with incremented retry counters.
-
- :param response: A response object, or None, if the server did not
- return a response.
- :type response: :class:`~urllib3.response.HTTPResponse`
- :param Exception error: An error encountered during the request, or
- None if the response was received successfully.
-
- :return: A new ``Retry`` object.
- """
- if self.total is False and error:
- # Disabled, indicate to re-raise the error.
- raise six.reraise(type(error), error, _stacktrace)
-
- total = self.total
- if total is not None:
- total -= 1
-
- connect = self.connect
- read = self.read
- redirect = self.redirect
- cause = 'unknown'
- status = None
- redirect_location = None
-
- if error and self._is_connection_error(error):
- # Connect retry?
- if connect is False:
- raise six.reraise(type(error), error, _stacktrace)
- elif connect is not None:
- connect -= 1
-
- elif error and self._is_read_error(error):
- # Read retry?
- if read is False or not self._is_method_retryable(method):
- raise six.reraise(type(error), error, _stacktrace)
- elif read is not None:
- read -= 1
-
- elif response and response.get_redirect_location():
- # Redirect retry?
- if redirect is not None:
- redirect -= 1
- cause = 'too many redirects'
- redirect_location = response.get_redirect_location()
- status = response.status
-
- else:
- # Incrementing because of a server error like a 500 in
- # status_forcelist and a the given method is in the whitelist
- cause = ResponseError.GENERIC_ERROR
- if response and response.status:
- cause = ResponseError.SPECIFIC_ERROR.format(
- status_code=response.status)
- status = response.status
-
- history = self.history + (RequestHistory(method, url, error, status, redirect_location),)
-
- new_retry = self.new(
- total=total,
- connect=connect, read=read, redirect=redirect,
- history=history)
-
- if new_retry.is_exhausted():
- raise MaxRetryError(_pool, url, error or ResponseError(cause))
-
- log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)
-
- return new_retry
-
- def __repr__(self):
- return ('{cls.__name__}(total={self.total}, connect={self.connect}, '
- 'read={self.read}, redirect={self.redirect})').format(
- cls=type(self), self=self)
-
-
-# For backwards compatibility (equivalent to pre-v1.9):
-Retry.DEFAULT = Retry(3)
+from __future__ import absolute_import
+import time
+import logging
+from collections import namedtuple
+from itertools import takewhile
+import email
+import re
+
+from ..exceptions import (
+ ConnectTimeoutError,
+ MaxRetryError,
+ ProtocolError,
+ ReadTimeoutError,
+ ResponseError,
+ InvalidHeader,
+)
+from ..packages import six
+
+
+log = logging.getLogger(__name__)
+
+# Data structure for representing the metadata of requests that result in a retry.
+RequestHistory = namedtuple('RequestHistory', ["method", "url", "error",
+ "status", "redirect_location"])
+
+
+class Retry(object):
+ """ Retry configuration.
+
+ Each retry attempt will create a new Retry object with updated values, so
+ they can be safely reused.
+
+ Retries can be defined as a default for a pool::
+
+ retries = Retry(connect=5, read=2, redirect=5)
+ http = PoolManager(retries=retries)
+ response = http.request('GET', 'http://example.com/')
+
+ Or per-request (which overrides the default for the pool)::
+
+ response = http.request('GET', 'http://example.com/', retries=Retry(10))
+
+ Retries can be disabled by passing ``False``::
+
+ response = http.request('GET', 'http://example.com/', retries=False)
+
+ Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless
+ retries are disabled, in which case the causing exception will be raised.
+
+ :param int total:
+ Total number of retries to allow. Takes precedence over other counts.
+
+ Set to ``None`` to remove this constraint and fall back on other
+ counts. It's a good idea to set this to some sensibly-high value to
+ account for unexpected edge cases and avoid infinite retry loops.
+
+ Set to ``0`` to fail on the first retry.
+
+ Set to ``False`` to disable and imply ``raise_on_redirect=False``.
+
+ :param int connect:
+ How many connection-related errors to retry on.
+
+ These are errors raised before the request is sent to the remote server,
+ which we assume has not triggered the server to process the request.
+
+ Set to ``0`` to fail on the first retry of this type.
+
+ :param int read:
+ How many times to retry on read errors.
+
+ These errors are raised after the request was sent to the server, so the
+ request may have side-effects.
+
+ Set to ``0`` to fail on the first retry of this type.
+
+ :param int redirect:
+ How many redirects to perform. Limit this to avoid infinite redirect
+ loops.
+
+ A redirect is a HTTP response with a status code 301, 302, 303, 307 or
+ 308.
+
+ Set to ``0`` to fail on the first retry of this type.
+
+ Set to ``False`` to disable and imply ``raise_on_redirect=False``.
+
+ :param iterable method_whitelist:
+ Set of uppercased HTTP method verbs that we should retry on.
+
+ By default, we only retry on methods which are considered to be
+ idempotent (multiple requests with the same parameters end with the
+ same state). See :attr:`Retry.DEFAULT_METHOD_WHITELIST`.
+
+ Set to a ``False`` value to retry on any verb.
+
+ :param iterable status_forcelist:
+ A set of integer HTTP status codes that we should force a retry on.
+ A retry is initiated if the request method is in ``method_whitelist``
+ and the response status code is in ``status_forcelist``.
+
+ By default, this is disabled with ``None``.
+
+ :param float backoff_factor:
+ A backoff factor to apply between attempts after the second try
+ (most errors are resolved immediately by a second try without a
+ delay). urllib3 will sleep for::
+
+ {backoff factor} * (2 ^ ({number of total retries} - 1))
+
+ seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep
+ for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer
+ than :attr:`Retry.BACKOFF_MAX`.
+
+ By default, backoff is disabled (set to 0).
+
+ :param bool raise_on_redirect: Whether, if the number of redirects is
+ exhausted, to raise a MaxRetryError, or to return a response with a
+ response code in the 3xx range.
+
+ :param bool raise_on_status: Similar meaning to ``raise_on_redirect``:
+ whether we should raise an exception, or return a response,
+ if status falls in ``status_forcelist`` range and retries have
+ been exhausted.
+
+ :param tuple history: The history of the request encountered during
+ each call to :meth:`~Retry.increment`. The list is in the order
+ the requests occurred. Each list item is of class :class:`RequestHistory`.
+
+ :param bool respect_retry_after_header:
+ Whether to respect Retry-After header on status codes defined as
+ :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not.
+
+ """
+
+ DEFAULT_METHOD_WHITELIST = frozenset([
+ 'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'])
+
+ RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])
+
+ #: Maximum backoff time.
+ BACKOFF_MAX = 120
+
+ def __init__(self, total=10, connect=None, read=None, redirect=None,
+ method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None,
+ backoff_factor=0, raise_on_redirect=True, raise_on_status=True,
+ history=None, respect_retry_after_header=True):
+
+ self.total = total
+ self.connect = connect
+ self.read = read
+
+ if redirect is False or total is False:
+ redirect = 0
+ raise_on_redirect = False
+
+ self.redirect = redirect
+ self.status_forcelist = status_forcelist or set()
+ self.method_whitelist = method_whitelist
+ self.backoff_factor = backoff_factor
+ self.raise_on_redirect = raise_on_redirect
+ self.raise_on_status = raise_on_status
+ self.history = history or tuple()
+ self.respect_retry_after_header = respect_retry_after_header
+
+ def new(self, **kw):
+ params = dict(
+ total=self.total,
+ connect=self.connect, read=self.read, redirect=self.redirect,
+ method_whitelist=self.method_whitelist,
+ status_forcelist=self.status_forcelist,
+ backoff_factor=self.backoff_factor,
+ raise_on_redirect=self.raise_on_redirect,
+ raise_on_status=self.raise_on_status,
+ history=self.history,
+ )
+ params.update(kw)
+ return type(self)(**params)
+
+ @classmethod
+ def from_int(cls, retries, redirect=True, default=None):
+ """ Backwards-compatibility for the old retries format."""
+ if retries is None:
+ retries = default if default is not None else cls.DEFAULT
+
+ if isinstance(retries, Retry):
+ return retries
+
+ redirect = bool(redirect) and None
+ new_retries = cls(retries, redirect=redirect)
+ log.debug("Converted retries value: %r -> %r", retries, new_retries)
+ return new_retries
+
+ def get_backoff_time(self):
+ """ Formula for computing the current backoff
+
+ :rtype: float
+ """
+ # We want to consider only the last consecutive errors sequence (Ignore redirects).
+ consecutive_errors_len = len(list(takewhile(lambda x: x.redirect_location is None,
+ reversed(self.history))))
+ if consecutive_errors_len <= 1:
+ return 0
+
+ backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1))
+ return min(self.BACKOFF_MAX, backoff_value)
+
+ def parse_retry_after(self, retry_after):
+ # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4
+ if re.match(r"^\s*[0-9]+\s*$", retry_after):
+ seconds = int(retry_after)
+ else:
+ retry_date_tuple = email.utils.parsedate(retry_after)
+ if retry_date_tuple is None:
+ raise InvalidHeader("Invalid Retry-After header: %s" % retry_after)
+ retry_date = time.mktime(retry_date_tuple)
+ seconds = retry_date - time.time()
+
+ if seconds < 0:
+ seconds = 0
+
+ return seconds
+
+ def get_retry_after(self, response):
+ """ Get the value of Retry-After in seconds. """
+
+ retry_after = response.getheader("Retry-After")
+
+ if retry_after is None:
+ return None
+
+ return self.parse_retry_after(retry_after)
+
+ def sleep_for_retry(self, response=None):
+ retry_after = self.get_retry_after(response)
+ if retry_after:
+ time.sleep(retry_after)
+ return True
+
+ return False
+
+ def _sleep_backoff(self):
+ backoff = self.get_backoff_time()
+ if backoff <= 0:
+ return
+ time.sleep(backoff)
+
+ def sleep(self, response=None):
+ """ Sleep between retry attempts.
+
+ This method will respect a server's ``Retry-After`` response header
+ and sleep the duration of the time requested. If that is not present, it
+ will use an exponential backoff. By default, the backoff factor is 0 and
+ this method will return immediately.
+ """
+
+ if response:
+ slept = self.sleep_for_retry(response)
+ if slept:
+ return
+
+ self._sleep_backoff()
+
+ def _is_connection_error(self, err):
+ """ Errors when we're fairly sure that the server did not receive the
+ request, so it should be safe to retry.
+ """
+ return isinstance(err, ConnectTimeoutError)
+
+ def _is_read_error(self, err):
+ """ Errors that occur after the request has been started, so we should
+ assume that the server began processing it.
+ """
+ return isinstance(err, (ReadTimeoutError, ProtocolError))
+
+ def _is_method_retryable(self, method):
+ """ Checks if a given HTTP method should be retried upon, depending if
+ it is included on the method whitelist.
+ """
+ if self.method_whitelist and method.upper() not in self.method_whitelist:
+ return False
+
+ return True
+
+ def is_retry(self, method, status_code, has_retry_after=False):
+ """ Is this method/status code retryable? (Based on whitelists and control
+ variables such as the number of total retries to allow, whether to
+ respect the Retry-After header, whether this header is present, and
+ whether the returned status code is on the list of status codes to
+ be retried upon on the presence of the aforementioned header)
+ """
+ if not self._is_method_retryable(method):
+ return False
+
+ if self.status_forcelist and status_code in self.status_forcelist:
+ return True
+
+ return (self.total and self.respect_retry_after_header and
+ has_retry_after and (status_code in self.RETRY_AFTER_STATUS_CODES))
+
+ def is_exhausted(self):
+ """ Are we out of retries? """
+ retry_counts = (self.total, self.connect, self.read, self.redirect)
+ retry_counts = list(filter(None, retry_counts))
+ if not retry_counts:
+ return False
+
+ return min(retry_counts) < 0
+
+ def increment(self, method=None, url=None, response=None, error=None,
+ _pool=None, _stacktrace=None):
+ """ Return a new Retry object with incremented retry counters.
+
+ :param response: A response object, or None, if the server did not
+ return a response.
+ :type response: :class:`~urllib3.response.HTTPResponse`
+ :param Exception error: An error encountered during the request, or
+ None if the response was received successfully.
+
+ :return: A new ``Retry`` object.
+ """
+ if self.total is False and error:
+ # Disabled, indicate to re-raise the error.
+ raise six.reraise(type(error), error, _stacktrace)
+
+ total = self.total
+ if total is not None:
+ total -= 1
+
+ connect = self.connect
+ read = self.read
+ redirect = self.redirect
+ cause = 'unknown'
+ status = None
+ redirect_location = None
+
+ if error and self._is_connection_error(error):
+ # Connect retry?
+ if connect is False:
+ raise six.reraise(type(error), error, _stacktrace)
+ elif connect is not None:
+ connect -= 1
+
+ elif error and self._is_read_error(error):
+ # Read retry?
+ if read is False or not self._is_method_retryable(method):
+ raise six.reraise(type(error), error, _stacktrace)
+ elif read is not None:
+ read -= 1
+
+ elif response and response.get_redirect_location():
+ # Redirect retry?
+ if redirect is not None:
+ redirect -= 1
+ cause = 'too many redirects'
+ redirect_location = response.get_redirect_location()
+ status = response.status
+
+ else:
+ # Incrementing because of a server error like a 500 in
+ # status_forcelist and a the given method is in the whitelist
+ cause = ResponseError.GENERIC_ERROR
+ if response and response.status:
+ cause = ResponseError.SPECIFIC_ERROR.format(
+ status_code=response.status)
+ status = response.status
+
+ history = self.history + (RequestHistory(method, url, error, status, redirect_location),)
+
+ new_retry = self.new(
+ total=total,
+ connect=connect, read=read, redirect=redirect,
+ history=history)
+
+ if new_retry.is_exhausted():
+ raise MaxRetryError(_pool, url, error or ResponseError(cause))
+
+ log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)
+
+ return new_retry
+
+ def __repr__(self):
+ return ('{cls.__name__}(total={self.total}, connect={self.connect}, '
+ 'read={self.read}, redirect={self.redirect})').format(
+ cls=type(self), self=self)
+
+
+# For backwards compatibility (equivalent to pre-v1.9):
+Retry.DEFAULT = Retry(3)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/selectors.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/selectors.py
index 0c41974..b381450 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/selectors.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/selectors.py
@@ -1,524 +1,529 @@
-# Backport of selectors.py from Python 3.5+ to support Python < 3.4
-# Also has the behavior specified in PEP 475 which is to retry syscalls
-# in the case of an EINTR error. This module is required because selectors34
-# does not follow this behavior and instead returns that no dile descriptor
-# events have occurred rather than retry the syscall. The decision to drop
-# support for select.devpoll is made to maintain 100% test coverage.
-
-import errno
-import math
-import select
-from collections import namedtuple, Mapping
-
-import time
-try:
- monotonic = time.monotonic
-except (AttributeError, ImportError): # Python 3.3<
- monotonic = time.time
-
-EVENT_READ = (1 << 0)
-EVENT_WRITE = (1 << 1)
-
-HAS_SELECT = True # Variable that shows whether the platform has a selector.
-_SYSCALL_SENTINEL = object() # Sentinel in case a system call returns None.
-
-
-class SelectorError(Exception):
- def __init__(self, errcode):
- super(SelectorError, self).__init__()
- self.errno = errcode
-
- def __repr__(self):
- return "".format(self.errno)
-
- def __str__(self):
- return self.__repr__()
-
-
-def _fileobj_to_fd(fileobj):
- """ Return a file descriptor from a file object. If
- given an integer will simply return that integer back. """
- if isinstance(fileobj, int):
- fd = fileobj
- else:
- try:
- fd = int(fileobj.fileno())
- except (AttributeError, TypeError, ValueError):
- raise ValueError("Invalid file object: {0!r}".format(fileobj))
- if fd < 0:
- raise ValueError("Invalid file descriptor: {0}".format(fd))
- return fd
-
-
-def _syscall_wrapper(func, recalc_timeout, *args, **kwargs):
- """ Wrapper function for syscalls that could fail due to EINTR.
- All functions should be retried if there is time left in the timeout
- in accordance with PEP 475. """
- timeout = kwargs.get("timeout", None)
- if timeout is None:
- expires = None
- recalc_timeout = False
- else:
- timeout = float(timeout)
- if timeout < 0.0: # Timeout less than 0 treated as no timeout.
- expires = None
- else:
- expires = monotonic() + timeout
-
- args = list(args)
- if recalc_timeout and "timeout" not in kwargs:
- raise ValueError(
- "Timeout must be in args or kwargs to be recalculated")
-
- result = _SYSCALL_SENTINEL
- while result is _SYSCALL_SENTINEL:
- try:
- result = func(*args, **kwargs)
- # OSError is thrown by select.select
- # IOError is thrown by select.epoll.poll
- # select.error is thrown by select.poll.poll
- # Aren't we thankful for Python 3.x rework for exceptions?
- except (OSError, IOError, select.error) as e:
- # select.error wasn't a subclass of OSError in the past.
- errcode = None
- if hasattr(e, "errno"):
- errcode = e.errno
- elif hasattr(e, "args"):
- errcode = e.args[0]
-
- # Also test for the Windows equivalent of EINTR.
- is_interrupt = (errcode == errno.EINTR or (hasattr(errno, "WSAEINTR") and
- errcode == errno.WSAEINTR))
-
- if is_interrupt:
- if expires is not None:
- current_time = monotonic()
- if current_time > expires:
- raise OSError(errno=errno.ETIMEDOUT)
- if recalc_timeout:
- if "timeout" in kwargs:
- kwargs["timeout"] = expires - current_time
- continue
- if errcode:
- raise SelectorError(errcode)
- else:
- raise
- return result
-
-
-SelectorKey = namedtuple('SelectorKey', ['fileobj', 'fd', 'events', 'data'])
-
-
-class _SelectorMapping(Mapping):
- """ Mapping of file objects to selector keys """
-
- def __init__(self, selector):
- self._selector = selector
-
- def __len__(self):
- return len(self._selector._fd_to_key)
-
- def __getitem__(self, fileobj):
- try:
- fd = self._selector._fileobj_lookup(fileobj)
- return self._selector._fd_to_key[fd]
- except KeyError:
- raise KeyError("{0!r} is not registered.".format(fileobj))
-
- def __iter__(self):
- return iter(self._selector._fd_to_key)
-
-
-class BaseSelector(object):
- """ Abstract Selector class
-
- A selector supports registering file objects to be monitored
- for specific I/O events.
-
- A file object is a file descriptor or any object with a
- `fileno()` method. An arbitrary object can be attached to the
- file object which can be used for example to store context info,
- a callback, etc.
-
- A selector can use various implementations (select(), poll(), epoll(),
- and kqueue()) depending on the platform. The 'DefaultSelector' class uses
- the most efficient implementation for the current platform.
- """
- def __init__(self):
- # Maps file descriptors to keys.
- self._fd_to_key = {}
-
- # Read-only mapping returned by get_map()
- self._map = _SelectorMapping(self)
-
- def _fileobj_lookup(self, fileobj):
- """ Return a file descriptor from a file object.
- This wraps _fileobj_to_fd() to do an exhaustive
- search in case the object is invalid but we still
- have it in our map. Used by unregister() so we can
- unregister an object that was previously registered
- even if it is closed. It is also used by _SelectorMapping
- """
- try:
- return _fileobj_to_fd(fileobj)
- except ValueError:
-
- # Search through all our mapped keys.
- for key in self._fd_to_key.values():
- if key.fileobj is fileobj:
- return key.fd
-
- # Raise ValueError after all.
- raise
-
- def register(self, fileobj, events, data=None):
- """ Register a file object for a set of events to monitor. """
- if (not events) or (events & ~(EVENT_READ | EVENT_WRITE)):
- raise ValueError("Invalid events: {0!r}".format(events))
-
- key = SelectorKey(fileobj, self._fileobj_lookup(fileobj), events, data)
-
- if key.fd in self._fd_to_key:
- raise KeyError("{0!r} (FD {1}) is already registered"
- .format(fileobj, key.fd))
-
- self._fd_to_key[key.fd] = key
- return key
-
- def unregister(self, fileobj):
- """ Unregister a file object from being monitored. """
- try:
- key = self._fd_to_key.pop(self._fileobj_lookup(fileobj))
- except KeyError:
- raise KeyError("{0!r} is not registered".format(fileobj))
- return key
-
- def modify(self, fileobj, events, data=None):
- """ Change a registered file object monitored events and data. """
- # NOTE: Some subclasses optimize this operation even further.
- try:
- key = self._fd_to_key[self._fileobj_lookup(fileobj)]
- except KeyError:
- raise KeyError("{0!r} is not registered".format(fileobj))
-
- if events != key.events:
- self.unregister(fileobj)
- key = self.register(fileobj, events, data)
-
- elif data != key.data:
- # Use a shortcut to update the data.
- key = key._replace(data=data)
- self._fd_to_key[key.fd] = key
-
- return key
-
- def select(self, timeout=None):
- """ Perform the actual selection until some monitored file objects
- are ready or the timeout expires. """
- raise NotImplementedError()
-
- def close(self):
- """ Close the selector. This must be called to ensure that all
- underlying resources are freed. """
- self._fd_to_key.clear()
- self._map = None
-
- def get_key(self, fileobj):
- """ Return the key associated with a registered file object. """
- mapping = self.get_map()
- if mapping is None:
- raise RuntimeError("Selector is closed")
- try:
- return mapping[fileobj]
- except KeyError:
- raise KeyError("{0!r} is not registered".format(fileobj))
-
- def get_map(self):
- """ Return a mapping of file objects to selector keys """
- return self._map
-
- def _key_from_fd(self, fd):
- """ Return the key associated to a given file descriptor
- Return None if it is not found. """
- try:
- return self._fd_to_key[fd]
- except KeyError:
- return None
-
- def __enter__(self):
- return self
-
- def __exit__(self, *args):
- self.close()
-
-
-# Almost all platforms have select.select()
-if hasattr(select, "select"):
- class SelectSelector(BaseSelector):
- """ Select-based selector. """
- def __init__(self):
- super(SelectSelector, self).__init__()
- self._readers = set()
- self._writers = set()
-
- def register(self, fileobj, events, data=None):
- key = super(SelectSelector, self).register(fileobj, events, data)
- if events & EVENT_READ:
- self._readers.add(key.fd)
- if events & EVENT_WRITE:
- self._writers.add(key.fd)
- return key
-
- def unregister(self, fileobj):
- key = super(SelectSelector, self).unregister(fileobj)
- self._readers.discard(key.fd)
- self._writers.discard(key.fd)
- return key
-
- def _select(self, r, w, timeout=None):
- """ Wrapper for select.select because timeout is a positional arg """
- return select.select(r, w, [], timeout)
-
- def select(self, timeout=None):
- # Selecting on empty lists on Windows errors out.
- if not len(self._readers) and not len(self._writers):
- return []
-
- timeout = None if timeout is None else max(timeout, 0.0)
- ready = []
- r, w, _ = _syscall_wrapper(self._select, True, self._readers,
- self._writers, timeout)
- r = set(r)
- w = set(w)
- for fd in r | w:
- events = 0
- if fd in r:
- events |= EVENT_READ
- if fd in w:
- events |= EVENT_WRITE
-
- key = self._key_from_fd(fd)
- if key:
- ready.append((key, events & key.events))
- return ready
-
-
-if hasattr(select, "poll"):
- class PollSelector(BaseSelector):
- """ Poll-based selector """
- def __init__(self):
- super(PollSelector, self).__init__()
- self._poll = select.poll()
-
- def register(self, fileobj, events, data=None):
- key = super(PollSelector, self).register(fileobj, events, data)
- event_mask = 0
- if events & EVENT_READ:
- event_mask |= select.POLLIN
- if events & EVENT_WRITE:
- event_mask |= select.POLLOUT
- self._poll.register(key.fd, event_mask)
- return key
-
- def unregister(self, fileobj):
- key = super(PollSelector, self).unregister(fileobj)
- self._poll.unregister(key.fd)
- return key
-
- def _wrap_poll(self, timeout=None):
- """ Wrapper function for select.poll.poll() so that
- _syscall_wrapper can work with only seconds. """
- if timeout is not None:
- if timeout <= 0:
- timeout = 0
- else:
- # select.poll.poll() has a resolution of 1 millisecond,
- # round away from zero to wait *at least* timeout seconds.
- timeout = math.ceil(timeout * 1e3)
-
- result = self._poll.poll(timeout)
- return result
-
- def select(self, timeout=None):
- ready = []
- fd_events = _syscall_wrapper(self._wrap_poll, True, timeout=timeout)
- for fd, event_mask in fd_events:
- events = 0
- if event_mask & ~select.POLLIN:
- events |= EVENT_WRITE
- if event_mask & ~select.POLLOUT:
- events |= EVENT_READ
-
- key = self._key_from_fd(fd)
- if key:
- ready.append((key, events & key.events))
-
- return ready
-
-
-if hasattr(select, "epoll"):
- class EpollSelector(BaseSelector):
- """ Epoll-based selector """
- def __init__(self):
- super(EpollSelector, self).__init__()
- self._epoll = select.epoll()
-
- def fileno(self):
- return self._epoll.fileno()
-
- def register(self, fileobj, events, data=None):
- key = super(EpollSelector, self).register(fileobj, events, data)
- events_mask = 0
- if events & EVENT_READ:
- events_mask |= select.EPOLLIN
- if events & EVENT_WRITE:
- events_mask |= select.EPOLLOUT
- _syscall_wrapper(self._epoll.register, False, key.fd, events_mask)
- return key
-
- def unregister(self, fileobj):
- key = super(EpollSelector, self).unregister(fileobj)
- try:
- _syscall_wrapper(self._epoll.unregister, False, key.fd)
- except SelectorError:
- # This can occur when the fd was closed since registry.
- pass
- return key
-
- def select(self, timeout=None):
- if timeout is not None:
- if timeout <= 0:
- timeout = 0.0
- else:
- # select.epoll.poll() has a resolution of 1 millisecond
- # but luckily takes seconds so we don't need a wrapper
- # like PollSelector. Just for better rounding.
- timeout = math.ceil(timeout * 1e3) * 1e-3
- timeout = float(timeout)
- else:
- timeout = -1.0 # epoll.poll() must have a float.
-
- # We always want at least 1 to ensure that select can be called
- # with no file descriptors registered. Otherwise will fail.
- max_events = max(len(self._fd_to_key), 1)
-
- ready = []
- fd_events = _syscall_wrapper(self._epoll.poll, True,
- timeout=timeout,
- maxevents=max_events)
- for fd, event_mask in fd_events:
- events = 0
- if event_mask & ~select.EPOLLIN:
- events |= EVENT_WRITE
- if event_mask & ~select.EPOLLOUT:
- events |= EVENT_READ
-
- key = self._key_from_fd(fd)
- if key:
- ready.append((key, events & key.events))
- return ready
-
- def close(self):
- self._epoll.close()
- super(EpollSelector, self).close()
-
-
-if hasattr(select, "kqueue"):
- class KqueueSelector(BaseSelector):
- """ Kqueue / Kevent-based selector """
- def __init__(self):
- super(KqueueSelector, self).__init__()
- self._kqueue = select.kqueue()
-
- def fileno(self):
- return self._kqueue.fileno()
-
- def register(self, fileobj, events, data=None):
- key = super(KqueueSelector, self).register(fileobj, events, data)
- if events & EVENT_READ:
- kevent = select.kevent(key.fd,
- select.KQ_FILTER_READ,
- select.KQ_EV_ADD)
-
- _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0)
-
- if events & EVENT_WRITE:
- kevent = select.kevent(key.fd,
- select.KQ_FILTER_WRITE,
- select.KQ_EV_ADD)
-
- _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0)
-
- return key
-
- def unregister(self, fileobj):
- key = super(KqueueSelector, self).unregister(fileobj)
- if key.events & EVENT_READ:
- kevent = select.kevent(key.fd,
- select.KQ_FILTER_READ,
- select.KQ_EV_DELETE)
- try:
- _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0)
- except SelectorError:
- pass
- if key.events & EVENT_WRITE:
- kevent = select.kevent(key.fd,
- select.KQ_FILTER_WRITE,
- select.KQ_EV_DELETE)
- try:
- _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0)
- except SelectorError:
- pass
-
- return key
-
- def select(self, timeout=None):
- if timeout is not None:
- timeout = max(timeout, 0)
-
- max_events = len(self._fd_to_key) * 2
- ready_fds = {}
-
- kevent_list = _syscall_wrapper(self._kqueue.control, True,
- None, max_events, timeout)
-
- for kevent in kevent_list:
- fd = kevent.ident
- event_mask = kevent.filter
- events = 0
- if event_mask == select.KQ_FILTER_READ:
- events |= EVENT_READ
- if event_mask == select.KQ_FILTER_WRITE:
- events |= EVENT_WRITE
-
- key = self._key_from_fd(fd)
- if key:
- if key.fd not in ready_fds:
- ready_fds[key.fd] = (key, events & key.events)
- else:
- old_events = ready_fds[key.fd][1]
- ready_fds[key.fd] = (key, (events | old_events) & key.events)
-
- return list(ready_fds.values())
-
- def close(self):
- self._kqueue.close()
- super(KqueueSelector, self).close()
-
-
-# Choose the best implementation, roughly:
-# kqueue == epoll > poll > select. Devpoll not supported. (See above)
-# select() also can't accept a FD > FD_SETSIZE (usually around 1024)
-if 'KqueueSelector' in globals(): # Platform-specific: Mac OS and BSD
- DefaultSelector = KqueueSelector
-elif 'EpollSelector' in globals(): # Platform-specific: Linux
- DefaultSelector = EpollSelector
-elif 'PollSelector' in globals(): # Platform-specific: Linux
- DefaultSelector = PollSelector
-elif 'SelectSelector' in globals(): # Platform-specific: Windows
- DefaultSelector = SelectSelector
-else: # Platform-specific: AppEngine
- def no_selector(_):
- raise ValueError("Platform does not have a selector")
- DefaultSelector = no_selector
- HAS_SELECT = False
+# Backport of selectors.py from Python 3.5+ to support Python < 3.4
+# Also has the behavior specified in PEP 475 which is to retry syscalls
+# in the case of an EINTR error. This module is required because selectors34
+# does not follow this behavior and instead returns that no dile descriptor
+# events have occurred rather than retry the syscall. The decision to drop
+# support for select.devpoll is made to maintain 100% test coverage.
+
+import errno
+import math
+import select
+from collections import namedtuple
+
+try:
+ from collections.abc import Mapping
+except ImportError:
+ from collections import Mapping
+
+import time
+try:
+ monotonic = time.monotonic
+except (AttributeError, ImportError): # Python 3.3<
+ monotonic = time.time
+
+EVENT_READ = (1 << 0)
+EVENT_WRITE = (1 << 1)
+
+HAS_SELECT = True # Variable that shows whether the platform has a selector.
+_SYSCALL_SENTINEL = object() # Sentinel in case a system call returns None.
+
+
+class SelectorError(Exception):
+ def __init__(self, errcode):
+ super(SelectorError, self).__init__()
+ self.errno = errcode
+
+ def __repr__(self):
+ return "".format(self.errno)
+
+ def __str__(self):
+ return self.__repr__()
+
+
+def _fileobj_to_fd(fileobj):
+ """ Return a file descriptor from a file object. If
+ given an integer will simply return that integer back. """
+ if isinstance(fileobj, int):
+ fd = fileobj
+ else:
+ try:
+ fd = int(fileobj.fileno())
+ except (AttributeError, TypeError, ValueError):
+ raise ValueError("Invalid file object: {0!r}".format(fileobj))
+ if fd < 0:
+ raise ValueError("Invalid file descriptor: {0}".format(fd))
+ return fd
+
+
+def _syscall_wrapper(func, recalc_timeout, *args, **kwargs):
+ """ Wrapper function for syscalls that could fail due to EINTR.
+ All functions should be retried if there is time left in the timeout
+ in accordance with PEP 475. """
+ timeout = kwargs.get("timeout", None)
+ if timeout is None:
+ expires = None
+ recalc_timeout = False
+ else:
+ timeout = float(timeout)
+ if timeout < 0.0: # Timeout less than 0 treated as no timeout.
+ expires = None
+ else:
+ expires = monotonic() + timeout
+
+ args = list(args)
+ if recalc_timeout and "timeout" not in kwargs:
+ raise ValueError(
+ "Timeout must be in args or kwargs to be recalculated")
+
+ result = _SYSCALL_SENTINEL
+ while result is _SYSCALL_SENTINEL:
+ try:
+ result = func(*args, **kwargs)
+ # OSError is thrown by select.select
+ # IOError is thrown by select.epoll.poll
+ # select.error is thrown by select.poll.poll
+ # Aren't we thankful for Python 3.x rework for exceptions?
+ except (OSError, IOError, select.error) as e:
+ # select.error wasn't a subclass of OSError in the past.
+ errcode = None
+ if hasattr(e, "errno"):
+ errcode = e.errno
+ elif hasattr(e, "args"):
+ errcode = e.args[0]
+
+ # Also test for the Windows equivalent of EINTR.
+ is_interrupt = (errcode == errno.EINTR or (hasattr(errno, "WSAEINTR") and
+ errcode == errno.WSAEINTR))
+
+ if is_interrupt:
+ if expires is not None:
+ current_time = monotonic()
+ if current_time > expires:
+ raise OSError(errno=errno.ETIMEDOUT)
+ if recalc_timeout:
+ if "timeout" in kwargs:
+ kwargs["timeout"] = expires - current_time
+ continue
+ if errcode:
+ raise SelectorError(errcode)
+ else:
+ raise
+ return result
+
+
+SelectorKey = namedtuple('SelectorKey', ['fileobj', 'fd', 'events', 'data'])
+
+
+class _SelectorMapping(Mapping):
+ """ Mapping of file objects to selector keys """
+
+ def __init__(self, selector):
+ self._selector = selector
+
+ def __len__(self):
+ return len(self._selector._fd_to_key)
+
+ def __getitem__(self, fileobj):
+ try:
+ fd = self._selector._fileobj_lookup(fileobj)
+ return self._selector._fd_to_key[fd]
+ except KeyError:
+ raise KeyError("{0!r} is not registered.".format(fileobj))
+
+ def __iter__(self):
+ return iter(self._selector._fd_to_key)
+
+
+class BaseSelector(object):
+ """ Abstract Selector class
+
+ A selector supports registering file objects to be monitored
+ for specific I/O events.
+
+ A file object is a file descriptor or any object with a
+ `fileno()` method. An arbitrary object can be attached to the
+ file object which can be used for example to store context info,
+ a callback, etc.
+
+ A selector can use various implementations (select(), poll(), epoll(),
+ and kqueue()) depending on the platform. The 'DefaultSelector' class uses
+ the most efficient implementation for the current platform.
+ """
+ def __init__(self):
+ # Maps file descriptors to keys.
+ self._fd_to_key = {}
+
+ # Read-only mapping returned by get_map()
+ self._map = _SelectorMapping(self)
+
+ def _fileobj_lookup(self, fileobj):
+ """ Return a file descriptor from a file object.
+ This wraps _fileobj_to_fd() to do an exhaustive
+ search in case the object is invalid but we still
+ have it in our map. Used by unregister() so we can
+ unregister an object that was previously registered
+ even if it is closed. It is also used by _SelectorMapping
+ """
+ try:
+ return _fileobj_to_fd(fileobj)
+ except ValueError:
+
+ # Search through all our mapped keys.
+ for key in self._fd_to_key.values():
+ if key.fileobj is fileobj:
+ return key.fd
+
+ # Raise ValueError after all.
+ raise
+
+ def register(self, fileobj, events, data=None):
+ """ Register a file object for a set of events to monitor. """
+ if (not events) or (events & ~(EVENT_READ | EVENT_WRITE)):
+ raise ValueError("Invalid events: {0!r}".format(events))
+
+ key = SelectorKey(fileobj, self._fileobj_lookup(fileobj), events, data)
+
+ if key.fd in self._fd_to_key:
+ raise KeyError("{0!r} (FD {1}) is already registered"
+ .format(fileobj, key.fd))
+
+ self._fd_to_key[key.fd] = key
+ return key
+
+ def unregister(self, fileobj):
+ """ Unregister a file object from being monitored. """
+ try:
+ key = self._fd_to_key.pop(self._fileobj_lookup(fileobj))
+ except KeyError:
+ raise KeyError("{0!r} is not registered".format(fileobj))
+ return key
+
+ def modify(self, fileobj, events, data=None):
+ """ Change a registered file object monitored events and data. """
+ # NOTE: Some subclasses optimize this operation even further.
+ try:
+ key = self._fd_to_key[self._fileobj_lookup(fileobj)]
+ except KeyError:
+ raise KeyError("{0!r} is not registered".format(fileobj))
+
+ if events != key.events:
+ self.unregister(fileobj)
+ key = self.register(fileobj, events, data)
+
+ elif data != key.data:
+ # Use a shortcut to update the data.
+ key = key._replace(data=data)
+ self._fd_to_key[key.fd] = key
+
+ return key
+
+ def select(self, timeout=None):
+ """ Perform the actual selection until some monitored file objects
+ are ready or the timeout expires. """
+ raise NotImplementedError()
+
+ def close(self):
+ """ Close the selector. This must be called to ensure that all
+ underlying resources are freed. """
+ self._fd_to_key.clear()
+ self._map = None
+
+ def get_key(self, fileobj):
+ """ Return the key associated with a registered file object. """
+ mapping = self.get_map()
+ if mapping is None:
+ raise RuntimeError("Selector is closed")
+ try:
+ return mapping[fileobj]
+ except KeyError:
+ raise KeyError("{0!r} is not registered".format(fileobj))
+
+ def get_map(self):
+ """ Return a mapping of file objects to selector keys """
+ return self._map
+
+ def _key_from_fd(self, fd):
+ """ Return the key associated to a given file descriptor
+ Return None if it is not found. """
+ try:
+ return self._fd_to_key[fd]
+ except KeyError:
+ return None
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.close()
+
+
+# Almost all platforms have select.select()
+if hasattr(select, "select"):
+ class SelectSelector(BaseSelector):
+ """ Select-based selector. """
+ def __init__(self):
+ super(SelectSelector, self).__init__()
+ self._readers = set()
+ self._writers = set()
+
+ def register(self, fileobj, events, data=None):
+ key = super(SelectSelector, self).register(fileobj, events, data)
+ if events & EVENT_READ:
+ self._readers.add(key.fd)
+ if events & EVENT_WRITE:
+ self._writers.add(key.fd)
+ return key
+
+ def unregister(self, fileobj):
+ key = super(SelectSelector, self).unregister(fileobj)
+ self._readers.discard(key.fd)
+ self._writers.discard(key.fd)
+ return key
+
+ def _select(self, r, w, timeout=None):
+ """ Wrapper for select.select because timeout is a positional arg """
+ return select.select(r, w, [], timeout)
+
+ def select(self, timeout=None):
+ # Selecting on empty lists on Windows errors out.
+ if not len(self._readers) and not len(self._writers):
+ return []
+
+ timeout = None if timeout is None else max(timeout, 0.0)
+ ready = []
+ r, w, _ = _syscall_wrapper(self._select, True, self._readers,
+ self._writers, timeout)
+ r = set(r)
+ w = set(w)
+ for fd in r | w:
+ events = 0
+ if fd in r:
+ events |= EVENT_READ
+ if fd in w:
+ events |= EVENT_WRITE
+
+ key = self._key_from_fd(fd)
+ if key:
+ ready.append((key, events & key.events))
+ return ready
+
+
+if hasattr(select, "poll"):
+ class PollSelector(BaseSelector):
+ """ Poll-based selector """
+ def __init__(self):
+ super(PollSelector, self).__init__()
+ self._poll = select.poll()
+
+ def register(self, fileobj, events, data=None):
+ key = super(PollSelector, self).register(fileobj, events, data)
+ event_mask = 0
+ if events & EVENT_READ:
+ event_mask |= select.POLLIN
+ if events & EVENT_WRITE:
+ event_mask |= select.POLLOUT
+ self._poll.register(key.fd, event_mask)
+ return key
+
+ def unregister(self, fileobj):
+ key = super(PollSelector, self).unregister(fileobj)
+ self._poll.unregister(key.fd)
+ return key
+
+ def _wrap_poll(self, timeout=None):
+ """ Wrapper function for select.poll.poll() so that
+ _syscall_wrapper can work with only seconds. """
+ if timeout is not None:
+ if timeout <= 0:
+ timeout = 0
+ else:
+ # select.poll.poll() has a resolution of 1 millisecond,
+ # round away from zero to wait *at least* timeout seconds.
+ timeout = math.ceil(timeout * 1e3)
+
+ result = self._poll.poll(timeout)
+ return result
+
+ def select(self, timeout=None):
+ ready = []
+ fd_events = _syscall_wrapper(self._wrap_poll, True, timeout=timeout)
+ for fd, event_mask in fd_events:
+ events = 0
+ if event_mask & ~select.POLLIN:
+ events |= EVENT_WRITE
+ if event_mask & ~select.POLLOUT:
+ events |= EVENT_READ
+
+ key = self._key_from_fd(fd)
+ if key:
+ ready.append((key, events & key.events))
+
+ return ready
+
+
+if hasattr(select, "epoll"):
+ class EpollSelector(BaseSelector):
+ """ Epoll-based selector """
+ def __init__(self):
+ super(EpollSelector, self).__init__()
+ self._epoll = select.epoll()
+
+ def fileno(self):
+ return self._epoll.fileno()
+
+ def register(self, fileobj, events, data=None):
+ key = super(EpollSelector, self).register(fileobj, events, data)
+ events_mask = 0
+ if events & EVENT_READ:
+ events_mask |= select.EPOLLIN
+ if events & EVENT_WRITE:
+ events_mask |= select.EPOLLOUT
+ _syscall_wrapper(self._epoll.register, False, key.fd, events_mask)
+ return key
+
+ def unregister(self, fileobj):
+ key = super(EpollSelector, self).unregister(fileobj)
+ try:
+ _syscall_wrapper(self._epoll.unregister, False, key.fd)
+ except SelectorError:
+ # This can occur when the fd was closed since registry.
+ pass
+ return key
+
+ def select(self, timeout=None):
+ if timeout is not None:
+ if timeout <= 0:
+ timeout = 0.0
+ else:
+ # select.epoll.poll() has a resolution of 1 millisecond
+ # but luckily takes seconds so we don't need a wrapper
+ # like PollSelector. Just for better rounding.
+ timeout = math.ceil(timeout * 1e3) * 1e-3
+ timeout = float(timeout)
+ else:
+ timeout = -1.0 # epoll.poll() must have a float.
+
+ # We always want at least 1 to ensure that select can be called
+ # with no file descriptors registered. Otherwise will fail.
+ max_events = max(len(self._fd_to_key), 1)
+
+ ready = []
+ fd_events = _syscall_wrapper(self._epoll.poll, True,
+ timeout=timeout,
+ maxevents=max_events)
+ for fd, event_mask in fd_events:
+ events = 0
+ if event_mask & ~select.EPOLLIN:
+ events |= EVENT_WRITE
+ if event_mask & ~select.EPOLLOUT:
+ events |= EVENT_READ
+
+ key = self._key_from_fd(fd)
+ if key:
+ ready.append((key, events & key.events))
+ return ready
+
+ def close(self):
+ self._epoll.close()
+ super(EpollSelector, self).close()
+
+
+if hasattr(select, "kqueue"):
+ class KqueueSelector(BaseSelector):
+ """ Kqueue / Kevent-based selector """
+ def __init__(self):
+ super(KqueueSelector, self).__init__()
+ self._kqueue = select.kqueue()
+
+ def fileno(self):
+ return self._kqueue.fileno()
+
+ def register(self, fileobj, events, data=None):
+ key = super(KqueueSelector, self).register(fileobj, events, data)
+ if events & EVENT_READ:
+ kevent = select.kevent(key.fd,
+ select.KQ_FILTER_READ,
+ select.KQ_EV_ADD)
+
+ _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0)
+
+ if events & EVENT_WRITE:
+ kevent = select.kevent(key.fd,
+ select.KQ_FILTER_WRITE,
+ select.KQ_EV_ADD)
+
+ _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0)
+
+ return key
+
+ def unregister(self, fileobj):
+ key = super(KqueueSelector, self).unregister(fileobj)
+ if key.events & EVENT_READ:
+ kevent = select.kevent(key.fd,
+ select.KQ_FILTER_READ,
+ select.KQ_EV_DELETE)
+ try:
+ _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0)
+ except SelectorError:
+ pass
+ if key.events & EVENT_WRITE:
+ kevent = select.kevent(key.fd,
+ select.KQ_FILTER_WRITE,
+ select.KQ_EV_DELETE)
+ try:
+ _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0)
+ except SelectorError:
+ pass
+
+ return key
+
+ def select(self, timeout=None):
+ if timeout is not None:
+ timeout = max(timeout, 0)
+
+ max_events = len(self._fd_to_key) * 2
+ ready_fds = {}
+
+ kevent_list = _syscall_wrapper(self._kqueue.control, True,
+ None, max_events, timeout)
+
+ for kevent in kevent_list:
+ fd = kevent.ident
+ event_mask = kevent.filter
+ events = 0
+ if event_mask == select.KQ_FILTER_READ:
+ events |= EVENT_READ
+ if event_mask == select.KQ_FILTER_WRITE:
+ events |= EVENT_WRITE
+
+ key = self._key_from_fd(fd)
+ if key:
+ if key.fd not in ready_fds:
+ ready_fds[key.fd] = (key, events & key.events)
+ else:
+ old_events = ready_fds[key.fd][1]
+ ready_fds[key.fd] = (key, (events | old_events) & key.events)
+
+ return list(ready_fds.values())
+
+ def close(self):
+ self._kqueue.close()
+ super(KqueueSelector, self).close()
+
+
+# Choose the best implementation, roughly:
+# kqueue == epoll > poll > select. Devpoll not supported. (See above)
+# select() also can't accept a FD > FD_SETSIZE (usually around 1024)
+if 'KqueueSelector' in globals(): # Platform-specific: Mac OS and BSD
+ DefaultSelector = KqueueSelector
+elif 'EpollSelector' in globals(): # Platform-specific: Linux
+ DefaultSelector = EpollSelector
+elif 'PollSelector' in globals(): # Platform-specific: Linux
+ DefaultSelector = PollSelector
+elif 'SelectSelector' in globals(): # Platform-specific: Windows
+ DefaultSelector = SelectSelector
+else: # Platform-specific: AppEngine
+ def no_selector(_):
+ raise ValueError("Platform does not have a selector")
+ DefaultSelector = no_selector
+ HAS_SELECT = False
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/ssl_.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/ssl_.py
index f85fede..c4c55df 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/ssl_.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/ssl_.py
@@ -1,336 +1,336 @@
-from __future__ import absolute_import
-import errno
-import warnings
-import hmac
-
-from binascii import hexlify, unhexlify
-from hashlib import md5, sha1, sha256
-
-from ..exceptions import SSLError, InsecurePlatformWarning, SNIMissingWarning
-
-
-SSLContext = None
-HAS_SNI = False
-IS_PYOPENSSL = False
-
-# Maps the length of a digest to a possible hash function producing this digest
-HASHFUNC_MAP = {
- 32: md5,
- 40: sha1,
- 64: sha256,
-}
-
-
-def _const_compare_digest_backport(a, b):
- """
- Compare two digests of equal length in constant time.
-
- The digests must be of type str/bytes.
- Returns True if the digests match, and False otherwise.
- """
- result = abs(len(a) - len(b))
- for l, r in zip(bytearray(a), bytearray(b)):
- result |= l ^ r
- return result == 0
-
-
-_const_compare_digest = getattr(hmac, 'compare_digest',
- _const_compare_digest_backport)
-
-
-try: # Test for SSL features
- import ssl
- from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
- from ssl import HAS_SNI # Has SNI?
-except ImportError:
- pass
-
-
-try:
- from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION
-except ImportError:
- OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000
- OP_NO_COMPRESSION = 0x20000
-
-# A secure default.
-# Sources for more information on TLS ciphers:
-#
-# - https://wiki.mozilla.org/Security/Server_Side_TLS
-# - https://www.ssllabs.com/projects/best-practices/index.html
-# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
-#
-# The general intent is:
-# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),
-# - prefer ECDHE over DHE for better performance,
-# - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and
-# security,
-# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common,
-# - disable NULL authentication, MD5 MACs and DSS for security reasons.
-DEFAULT_CIPHERS = ':'.join([
- 'ECDH+AESGCM',
- 'ECDH+CHACHA20',
- 'DH+AESGCM',
- 'DH+CHACHA20',
- 'ECDH+AES256',
- 'DH+AES256',
- 'ECDH+AES128',
- 'DH+AES',
- 'RSA+AESGCM',
- 'RSA+AES',
- '!aNULL',
- '!eNULL',
- '!MD5',
-])
-
-try:
- from ssl import SSLContext # Modern SSL?
-except ImportError:
- import sys
-
- class SSLContext(object): # Platform-specific: Python 2 & 3.1
- supports_set_ciphers = ((2, 7) <= sys.version_info < (3,) or
- (3, 2) <= sys.version_info)
-
- def __init__(self, protocol_version):
- self.protocol = protocol_version
- # Use default values from a real SSLContext
- self.check_hostname = False
- self.verify_mode = ssl.CERT_NONE
- self.ca_certs = None
- self.options = 0
- self.certfile = None
- self.keyfile = None
- self.ciphers = None
-
- def load_cert_chain(self, certfile, keyfile):
- self.certfile = certfile
- self.keyfile = keyfile
-
- def load_verify_locations(self, cafile=None, capath=None):
- self.ca_certs = cafile
-
- if capath is not None:
- raise SSLError("CA directories not supported in older Pythons")
-
- def set_ciphers(self, cipher_suite):
- if not self.supports_set_ciphers:
- raise TypeError(
- 'Your version of Python does not support setting '
- 'a custom cipher suite. Please upgrade to Python '
- '2.7, 3.2, or later if you need this functionality.'
- )
- self.ciphers = cipher_suite
-
- def wrap_socket(self, socket, server_hostname=None, server_side=False):
- warnings.warn(
- 'A true SSLContext object is not available. This prevents '
- 'urllib3 from configuring SSL appropriately and may cause '
- 'certain SSL connections to fail. You can upgrade to a newer '
- 'version of Python to solve this. For more information, see '
- 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html'
- '#ssl-warnings',
- InsecurePlatformWarning
- )
- kwargs = {
- 'keyfile': self.keyfile,
- 'certfile': self.certfile,
- 'ca_certs': self.ca_certs,
- 'cert_reqs': self.verify_mode,
- 'ssl_version': self.protocol,
- 'server_side': server_side,
- }
- if self.supports_set_ciphers: # Platform-specific: Python 2.7+
- return wrap_socket(socket, ciphers=self.ciphers, **kwargs)
- else: # Platform-specific: Python 2.6
- return wrap_socket(socket, **kwargs)
-
-
-def assert_fingerprint(cert, fingerprint):
- """
- Checks if given fingerprint matches the supplied certificate.
-
- :param cert:
- Certificate as bytes object.
- :param fingerprint:
- Fingerprint as string of hexdigits, can be interspersed by colons.
- """
-
- fingerprint = fingerprint.replace(':', '').lower()
- digest_length = len(fingerprint)
- hashfunc = HASHFUNC_MAP.get(digest_length)
- if not hashfunc:
- raise SSLError(
- 'Fingerprint of invalid length: {0}'.format(fingerprint))
-
- # We need encode() here for py32; works on py2 and p33.
- fingerprint_bytes = unhexlify(fingerprint.encode())
-
- cert_digest = hashfunc(cert).digest()
-
- if not _const_compare_digest(cert_digest, fingerprint_bytes):
- raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".'
- .format(fingerprint, hexlify(cert_digest)))
-
-
-def resolve_cert_reqs(candidate):
- """
- Resolves the argument to a numeric constant, which can be passed to
- the wrap_socket function/method from the ssl module.
- Defaults to :data:`ssl.CERT_NONE`.
- If given a string it is assumed to be the name of the constant in the
- :mod:`ssl` module or its abbrevation.
- (So you can specify `REQUIRED` instead of `CERT_REQUIRED`.
- If it's neither `None` nor a string we assume it is already the numeric
- constant which can directly be passed to wrap_socket.
- """
- if candidate is None:
- return CERT_NONE
-
- if isinstance(candidate, str):
- res = getattr(ssl, candidate, None)
- if res is None:
- res = getattr(ssl, 'CERT_' + candidate)
- return res
-
- return candidate
-
-
-def resolve_ssl_version(candidate):
- """
- like resolve_cert_reqs
- """
- if candidate is None:
- return PROTOCOL_SSLv23
-
- if isinstance(candidate, str):
- res = getattr(ssl, candidate, None)
- if res is None:
- res = getattr(ssl, 'PROTOCOL_' + candidate)
- return res
-
- return candidate
-
-
-def create_urllib3_context(ssl_version=None, cert_reqs=None,
- options=None, ciphers=None):
- """All arguments have the same meaning as ``ssl_wrap_socket``.
-
- By default, this function does a lot of the same work that
- ``ssl.create_default_context`` does on Python 3.4+. It:
-
- - Disables SSLv2, SSLv3, and compression
- - Sets a restricted set of server ciphers
-
- If you wish to enable SSLv3, you can do::
-
- from urllib3.util import ssl_
- context = ssl_.create_urllib3_context()
- context.options &= ~ssl_.OP_NO_SSLv3
-
- You can do the same to enable compression (substituting ``COMPRESSION``
- for ``SSLv3`` in the last line above).
-
- :param ssl_version:
- The desired protocol version to use. This will default to
- PROTOCOL_SSLv23 which will negotiate the highest protocol that both
- the server and your installation of OpenSSL support.
- :param cert_reqs:
- Whether to require the certificate verification. This defaults to
- ``ssl.CERT_REQUIRED``.
- :param options:
- Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``,
- ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``.
- :param ciphers:
- Which cipher suites to allow the server to select.
- :returns:
- Constructed SSLContext object with specified options
- :rtype: SSLContext
- """
- context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23)
-
- # Setting the default here, as we may have no ssl module on import
- cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs
-
- if options is None:
- options = 0
- # SSLv2 is easily broken and is considered harmful and dangerous
- options |= OP_NO_SSLv2
- # SSLv3 has several problems and is now dangerous
- options |= OP_NO_SSLv3
- # Disable compression to prevent CRIME attacks for OpenSSL 1.0+
- # (issue #309)
- options |= OP_NO_COMPRESSION
-
- context.options |= options
-
- if getattr(context, 'supports_set_ciphers', True): # Platform-specific: Python 2.6
- context.set_ciphers(ciphers or DEFAULT_CIPHERS)
-
- context.verify_mode = cert_reqs
- if getattr(context, 'check_hostname', None) is not None: # Platform-specific: Python 3.2
- # We do our own verification, including fingerprints and alternative
- # hostnames. So disable it here
- context.check_hostname = False
- return context
-
-
-def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
- ca_certs=None, server_hostname=None,
- ssl_version=None, ciphers=None, ssl_context=None,
- ca_cert_dir=None):
- """
- All arguments except for server_hostname, ssl_context, and ca_cert_dir have
- the same meaning as they do when using :func:`ssl.wrap_socket`.
-
- :param server_hostname:
- When SNI is supported, the expected hostname of the certificate
- :param ssl_context:
- A pre-made :class:`SSLContext` object. If none is provided, one will
- be created using :func:`create_urllib3_context`.
- :param ciphers:
- A string of ciphers we wish the client to support. This is not
- supported on Python 2.6 as the ssl module does not support it.
- :param ca_cert_dir:
- A directory containing CA certificates in multiple separate files, as
- supported by OpenSSL's -CApath flag or the capath argument to
- SSLContext.load_verify_locations().
- """
- context = ssl_context
- if context is None:
- # Note: This branch of code and all the variables in it are no longer
- # used by urllib3 itself. We should consider deprecating and removing
- # this code.
- context = create_urllib3_context(ssl_version, cert_reqs,
- ciphers=ciphers)
-
- if ca_certs or ca_cert_dir:
- try:
- context.load_verify_locations(ca_certs, ca_cert_dir)
- except IOError as e: # Platform-specific: Python 2.6, 2.7, 3.2
- raise SSLError(e)
- # Py33 raises FileNotFoundError which subclasses OSError
- # These are not equivalent unless we check the errno attribute
- except OSError as e: # Platform-specific: Python 3.3 and beyond
- if e.errno == errno.ENOENT:
- raise SSLError(e)
- raise
- elif getattr(context, 'load_default_certs', None) is not None:
- # try to load OS default certs; works well on Windows (require Python3.4+)
- context.load_default_certs()
-
- if certfile:
- context.load_cert_chain(certfile, keyfile)
- if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI
- return context.wrap_socket(sock, server_hostname=server_hostname)
-
- warnings.warn(
- 'An HTTPS request has been made, but the SNI (Subject Name '
- 'Indication) extension to TLS is not available on this platform. '
- 'This may cause the server to present an incorrect TLS '
- 'certificate, which can cause validation failures. You can upgrade to '
- 'a newer version of Python to solve this. For more information, see '
- 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html'
- '#ssl-warnings',
- SNIMissingWarning
- )
- return context.wrap_socket(sock)
+from __future__ import absolute_import
+import errno
+import warnings
+import hmac
+
+from binascii import hexlify, unhexlify
+from hashlib import md5, sha1, sha256
+
+from ..exceptions import SSLError, InsecurePlatformWarning, SNIMissingWarning
+
+
+SSLContext = None
+HAS_SNI = False
+IS_PYOPENSSL = False
+
+# Maps the length of a digest to a possible hash function producing this digest
+HASHFUNC_MAP = {
+ 32: md5,
+ 40: sha1,
+ 64: sha256,
+}
+
+
+def _const_compare_digest_backport(a, b):
+ """
+ Compare two digests of equal length in constant time.
+
+ The digests must be of type str/bytes.
+ Returns True if the digests match, and False otherwise.
+ """
+ result = abs(len(a) - len(b))
+ for l, r in zip(bytearray(a), bytearray(b)):
+ result |= l ^ r
+ return result == 0
+
+
+_const_compare_digest = getattr(hmac, 'compare_digest',
+ _const_compare_digest_backport)
+
+
+try: # Test for SSL features
+ import ssl
+ from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
+ from ssl import HAS_SNI # Has SNI?
+except ImportError:
+ pass
+
+
+try:
+ from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION
+except ImportError:
+ OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000
+ OP_NO_COMPRESSION = 0x20000
+
+# A secure default.
+# Sources for more information on TLS ciphers:
+#
+# - https://wiki.mozilla.org/Security/Server_Side_TLS
+# - https://www.ssllabs.com/projects/best-practices/index.html
+# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
+#
+# The general intent is:
+# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),
+# - prefer ECDHE over DHE for better performance,
+# - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and
+# security,
+# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common,
+# - disable NULL authentication, MD5 MACs and DSS for security reasons.
+DEFAULT_CIPHERS = ':'.join([
+ 'ECDH+AESGCM',
+ 'ECDH+CHACHA20',
+ 'DH+AESGCM',
+ 'DH+CHACHA20',
+ 'ECDH+AES256',
+ 'DH+AES256',
+ 'ECDH+AES128',
+ 'DH+AES',
+ 'RSA+AESGCM',
+ 'RSA+AES',
+ '!aNULL',
+ '!eNULL',
+ '!MD5',
+])
+
+try:
+ from ssl import SSLContext # Modern SSL?
+except ImportError:
+ import sys
+
+ class SSLContext(object): # Platform-specific: Python 2 & 3.1
+ supports_set_ciphers = ((2, 7) <= sys.version_info < (3,) or
+ (3, 2) <= sys.version_info)
+
+ def __init__(self, protocol_version):
+ self.protocol = protocol_version
+ # Use default values from a real SSLContext
+ self.check_hostname = False
+ self.verify_mode = ssl.CERT_NONE
+ self.ca_certs = None
+ self.options = 0
+ self.certfile = None
+ self.keyfile = None
+ self.ciphers = None
+
+ def load_cert_chain(self, certfile, keyfile):
+ self.certfile = certfile
+ self.keyfile = keyfile
+
+ def load_verify_locations(self, cafile=None, capath=None):
+ self.ca_certs = cafile
+
+ if capath is not None:
+ raise SSLError("CA directories not supported in older Pythons")
+
+ def set_ciphers(self, cipher_suite):
+ if not self.supports_set_ciphers:
+ raise TypeError(
+ 'Your version of Python does not support setting '
+ 'a custom cipher suite. Please upgrade to Python '
+ '2.7, 3.2, or later if you need this functionality.'
+ )
+ self.ciphers = cipher_suite
+
+ def wrap_socket(self, socket, server_hostname=None, server_side=False):
+ warnings.warn(
+ 'A true SSLContext object is not available. This prevents '
+ 'urllib3 from configuring SSL appropriately and may cause '
+ 'certain SSL connections to fail. You can upgrade to a newer '
+ 'version of Python to solve this. For more information, see '
+ 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html'
+ '#ssl-warnings',
+ InsecurePlatformWarning
+ )
+ kwargs = {
+ 'keyfile': self.keyfile,
+ 'certfile': self.certfile,
+ 'ca_certs': self.ca_certs,
+ 'cert_reqs': self.verify_mode,
+ 'ssl_version': self.protocol,
+ 'server_side': server_side,
+ }
+ if self.supports_set_ciphers: # Platform-specific: Python 2.7+
+ return wrap_socket(socket, ciphers=self.ciphers, **kwargs)
+ else: # Platform-specific: Python 2.6
+ return wrap_socket(socket, **kwargs)
+
+
+def assert_fingerprint(cert, fingerprint):
+ """
+ Checks if given fingerprint matches the supplied certificate.
+
+ :param cert:
+ Certificate as bytes object.
+ :param fingerprint:
+ Fingerprint as string of hexdigits, can be interspersed by colons.
+ """
+
+ fingerprint = fingerprint.replace(':', '').lower()
+ digest_length = len(fingerprint)
+ hashfunc = HASHFUNC_MAP.get(digest_length)
+ if not hashfunc:
+ raise SSLError(
+ 'Fingerprint of invalid length: {0}'.format(fingerprint))
+
+ # We need encode() here for py32; works on py2 and p33.
+ fingerprint_bytes = unhexlify(fingerprint.encode())
+
+ cert_digest = hashfunc(cert).digest()
+
+ if not _const_compare_digest(cert_digest, fingerprint_bytes):
+ raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".'
+ .format(fingerprint, hexlify(cert_digest)))
+
+
+def resolve_cert_reqs(candidate):
+ """
+ Resolves the argument to a numeric constant, which can be passed to
+ the wrap_socket function/method from the ssl module.
+ Defaults to :data:`ssl.CERT_NONE`.
+ If given a string it is assumed to be the name of the constant in the
+ :mod:`ssl` module or its abbrevation.
+ (So you can specify `REQUIRED` instead of `CERT_REQUIRED`.
+ If it's neither `None` nor a string we assume it is already the numeric
+ constant which can directly be passed to wrap_socket.
+ """
+ if candidate is None:
+ return CERT_NONE
+
+ if isinstance(candidate, str):
+ res = getattr(ssl, candidate, None)
+ if res is None:
+ res = getattr(ssl, 'CERT_' + candidate)
+ return res
+
+ return candidate
+
+
+def resolve_ssl_version(candidate):
+ """
+ like resolve_cert_reqs
+ """
+ if candidate is None:
+ return PROTOCOL_SSLv23
+
+ if isinstance(candidate, str):
+ res = getattr(ssl, candidate, None)
+ if res is None:
+ res = getattr(ssl, 'PROTOCOL_' + candidate)
+ return res
+
+ return candidate
+
+
+def create_urllib3_context(ssl_version=None, cert_reqs=None,
+ options=None, ciphers=None):
+ """All arguments have the same meaning as ``ssl_wrap_socket``.
+
+ By default, this function does a lot of the same work that
+ ``ssl.create_default_context`` does on Python 3.4+. It:
+
+ - Disables SSLv2, SSLv3, and compression
+ - Sets a restricted set of server ciphers
+
+ If you wish to enable SSLv3, you can do::
+
+ from urllib3.util import ssl_
+ context = ssl_.create_urllib3_context()
+ context.options &= ~ssl_.OP_NO_SSLv3
+
+ You can do the same to enable compression (substituting ``COMPRESSION``
+ for ``SSLv3`` in the last line above).
+
+ :param ssl_version:
+ The desired protocol version to use. This will default to
+ PROTOCOL_SSLv23 which will negotiate the highest protocol that both
+ the server and your installation of OpenSSL support.
+ :param cert_reqs:
+ Whether to require the certificate verification. This defaults to
+ ``ssl.CERT_REQUIRED``.
+ :param options:
+ Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``,
+ ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``.
+ :param ciphers:
+ Which cipher suites to allow the server to select.
+ :returns:
+ Constructed SSLContext object with specified options
+ :rtype: SSLContext
+ """
+ context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23)
+
+ # Setting the default here, as we may have no ssl module on import
+ cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs
+
+ if options is None:
+ options = 0
+ # SSLv2 is easily broken and is considered harmful and dangerous
+ options |= OP_NO_SSLv2
+ # SSLv3 has several problems and is now dangerous
+ options |= OP_NO_SSLv3
+ # Disable compression to prevent CRIME attacks for OpenSSL 1.0+
+ # (issue #309)
+ options |= OP_NO_COMPRESSION
+
+ context.options |= options
+
+ if getattr(context, 'supports_set_ciphers', True): # Platform-specific: Python 2.6
+ context.set_ciphers(ciphers or DEFAULT_CIPHERS)
+
+ context.verify_mode = cert_reqs
+ if getattr(context, 'check_hostname', None) is not None: # Platform-specific: Python 3.2
+ # We do our own verification, including fingerprints and alternative
+ # hostnames. So disable it here
+ context.check_hostname = False
+ return context
+
+
+def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
+ ca_certs=None, server_hostname=None,
+ ssl_version=None, ciphers=None, ssl_context=None,
+ ca_cert_dir=None):
+ """
+ All arguments except for server_hostname, ssl_context, and ca_cert_dir have
+ the same meaning as they do when using :func:`ssl.wrap_socket`.
+
+ :param server_hostname:
+ When SNI is supported, the expected hostname of the certificate
+ :param ssl_context:
+ A pre-made :class:`SSLContext` object. If none is provided, one will
+ be created using :func:`create_urllib3_context`.
+ :param ciphers:
+ A string of ciphers we wish the client to support. This is not
+ supported on Python 2.6 as the ssl module does not support it.
+ :param ca_cert_dir:
+ A directory containing CA certificates in multiple separate files, as
+ supported by OpenSSL's -CApath flag or the capath argument to
+ SSLContext.load_verify_locations().
+ """
+ context = ssl_context
+ if context is None:
+ # Note: This branch of code and all the variables in it are no longer
+ # used by urllib3 itself. We should consider deprecating and removing
+ # this code.
+ context = create_urllib3_context(ssl_version, cert_reqs,
+ ciphers=ciphers)
+
+ if ca_certs or ca_cert_dir:
+ try:
+ context.load_verify_locations(ca_certs, ca_cert_dir)
+ except IOError as e: # Platform-specific: Python 2.6, 2.7, 3.2
+ raise SSLError(e)
+ # Py33 raises FileNotFoundError which subclasses OSError
+ # These are not equivalent unless we check the errno attribute
+ except OSError as e: # Platform-specific: Python 3.3 and beyond
+ if e.errno == errno.ENOENT:
+ raise SSLError(e)
+ raise
+ elif getattr(context, 'load_default_certs', None) is not None:
+ # try to load OS default certs; works well on Windows (require Python3.4+)
+ context.load_default_certs()
+
+ if certfile:
+ context.load_cert_chain(certfile, keyfile)
+ if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI
+ return context.wrap_socket(sock, server_hostname=server_hostname)
+
+ warnings.warn(
+ 'An HTTPS request has been made, but the SNI (Subject Name '
+ 'Indication) extension to TLS is not available on this platform. '
+ 'This may cause the server to present an incorrect TLS '
+ 'certificate, which can cause validation failures. You can upgrade to '
+ 'a newer version of Python to solve this. For more information, see '
+ 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html'
+ '#ssl-warnings',
+ SNIMissingWarning
+ )
+ return context.wrap_socket(sock)
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/timeout.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/timeout.py
index 9c2e6ef..cec817e 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/timeout.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/timeout.py
@@ -1,242 +1,242 @@
-from __future__ import absolute_import
-# The default socket timeout, used by httplib to indicate that no timeout was
-# specified by the user
-from socket import _GLOBAL_DEFAULT_TIMEOUT
-import time
-
-from ..exceptions import TimeoutStateError
-
-# A sentinel value to indicate that no timeout was specified by the user in
-# urllib3
-_Default = object()
-
-
-# Use time.monotonic if available.
-current_time = getattr(time, "monotonic", time.time)
-
-
-class Timeout(object):
- """ Timeout configuration.
-
- Timeouts can be defined as a default for a pool::
-
- timeout = Timeout(connect=2.0, read=7.0)
- http = PoolManager(timeout=timeout)
- response = http.request('GET', 'http://example.com/')
-
- Or per-request (which overrides the default for the pool)::
-
- response = http.request('GET', 'http://example.com/', timeout=Timeout(10))
-
- Timeouts can be disabled by setting all the parameters to ``None``::
-
- no_timeout = Timeout(connect=None, read=None)
- response = http.request('GET', 'http://example.com/, timeout=no_timeout)
-
-
- :param total:
- This combines the connect and read timeouts into one; the read timeout
- will be set to the time leftover from the connect attempt. In the
- event that both a connect timeout and a total are specified, or a read
- timeout and a total are specified, the shorter timeout will be applied.
-
- Defaults to None.
-
- :type total: integer, float, or None
-
- :param connect:
- The maximum amount of time to wait for a connection attempt to a server
- to succeed. Omitting the parameter will default the connect timeout to
- the system default, probably `the global default timeout in socket.py
- `_.
- None will set an infinite timeout for connection attempts.
-
- :type connect: integer, float, or None
-
- :param read:
- The maximum amount of time to wait between consecutive
- read operations for a response from the server. Omitting
- the parameter will default the read timeout to the system
- default, probably `the global default timeout in socket.py
- `_.
- None will set an infinite timeout.
-
- :type read: integer, float, or None
-
- .. note::
-
- Many factors can affect the total amount of time for urllib3 to return
- an HTTP response.
-
- For example, Python's DNS resolver does not obey the timeout specified
- on the socket. Other factors that can affect total request time include
- high CPU load, high swap, the program running at a low priority level,
- or other behaviors.
-
- In addition, the read and total timeouts only measure the time between
- read operations on the socket connecting the client and the server,
- not the total amount of time for the request to return a complete
- response. For most requests, the timeout is raised because the server
- has not sent the first byte in the specified time. This is not always
- the case; if a server streams one byte every fifteen seconds, a timeout
- of 20 seconds will not trigger, even though the request will take
- several minutes to complete.
-
- If your goal is to cut off any request after a set amount of wall clock
- time, consider having a second "watcher" thread to cut off a slow
- request.
- """
-
- #: A sentinel object representing the default timeout value
- DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT
-
- def __init__(self, total=None, connect=_Default, read=_Default):
- self._connect = self._validate_timeout(connect, 'connect')
- self._read = self._validate_timeout(read, 'read')
- self.total = self._validate_timeout(total, 'total')
- self._start_connect = None
-
- def __str__(self):
- return '%s(connect=%r, read=%r, total=%r)' % (
- type(self).__name__, self._connect, self._read, self.total)
-
- @classmethod
- def _validate_timeout(cls, value, name):
- """ Check that a timeout attribute is valid.
-
- :param value: The timeout value to validate
- :param name: The name of the timeout attribute to validate. This is
- used to specify in error messages.
- :return: The validated and casted version of the given value.
- :raises ValueError: If it is a numeric value less than or equal to
- zero, or the type is not an integer, float, or None.
- """
- if value is _Default:
- return cls.DEFAULT_TIMEOUT
-
- if value is None or value is cls.DEFAULT_TIMEOUT:
- return value
-
- if isinstance(value, bool):
- raise ValueError("Timeout cannot be a boolean value. It must "
- "be an int, float or None.")
- try:
- float(value)
- except (TypeError, ValueError):
- raise ValueError("Timeout value %s was %s, but it must be an "
- "int, float or None." % (name, value))
-
- try:
- if value <= 0:
- raise ValueError("Attempted to set %s timeout to %s, but the "
- "timeout cannot be set to a value less "
- "than or equal to 0." % (name, value))
- except TypeError: # Python 3
- raise ValueError("Timeout value %s was %s, but it must be an "
- "int, float or None." % (name, value))
-
- return value
-
- @classmethod
- def from_float(cls, timeout):
- """ Create a new Timeout from a legacy timeout value.
-
- The timeout value used by httplib.py sets the same timeout on the
- connect(), and recv() socket requests. This creates a :class:`Timeout`
- object that sets the individual timeouts to the ``timeout`` value
- passed to this function.
-
- :param timeout: The legacy timeout value.
- :type timeout: integer, float, sentinel default object, or None
- :return: Timeout object
- :rtype: :class:`Timeout`
- """
- return Timeout(read=timeout, connect=timeout)
-
- def clone(self):
- """ Create a copy of the timeout object
-
- Timeout properties are stored per-pool but each request needs a fresh
- Timeout object to ensure each one has its own start/stop configured.
-
- :return: a copy of the timeout object
- :rtype: :class:`Timeout`
- """
- # We can't use copy.deepcopy because that will also create a new object
- # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to
- # detect the user default.
- return Timeout(connect=self._connect, read=self._read,
- total=self.total)
-
- def start_connect(self):
- """ Start the timeout clock, used during a connect() attempt
-
- :raises urllib3.exceptions.TimeoutStateError: if you attempt
- to start a timer that has been started already.
- """
- if self._start_connect is not None:
- raise TimeoutStateError("Timeout timer has already been started.")
- self._start_connect = current_time()
- return self._start_connect
-
- def get_connect_duration(self):
- """ Gets the time elapsed since the call to :meth:`start_connect`.
-
- :return: Elapsed time.
- :rtype: float
- :raises urllib3.exceptions.TimeoutStateError: if you attempt
- to get duration for a timer that hasn't been started.
- """
- if self._start_connect is None:
- raise TimeoutStateError("Can't get connect duration for timer "
- "that has not started.")
- return current_time() - self._start_connect
-
- @property
- def connect_timeout(self):
- """ Get the value to use when setting a connection timeout.
-
- This will be a positive float or integer, the value None
- (never timeout), or the default system timeout.
-
- :return: Connect timeout.
- :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
- """
- if self.total is None:
- return self._connect
-
- if self._connect is None or self._connect is self.DEFAULT_TIMEOUT:
- return self.total
-
- return min(self._connect, self.total)
-
- @property
- def read_timeout(self):
- """ Get the value for the read timeout.
-
- This assumes some time has elapsed in the connection timeout and
- computes the read timeout appropriately.
-
- If self.total is set, the read timeout is dependent on the amount of
- time taken by the connect timeout. If the connection time has not been
- established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be
- raised.
-
- :return: Value to use for the read timeout.
- :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
- :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect`
- has not yet been called on this object.
- """
- if (self.total is not None and
- self.total is not self.DEFAULT_TIMEOUT and
- self._read is not None and
- self._read is not self.DEFAULT_TIMEOUT):
- # In case the connect timeout has not yet been established.
- if self._start_connect is None:
- return self._read
- return max(0, min(self.total - self.get_connect_duration(),
- self._read))
- elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT:
- return max(0, self.total - self.get_connect_duration())
- else:
- return self._read
+from __future__ import absolute_import
+# The default socket timeout, used by httplib to indicate that no timeout was
+# specified by the user
+from socket import _GLOBAL_DEFAULT_TIMEOUT
+import time
+
+from ..exceptions import TimeoutStateError
+
+# A sentinel value to indicate that no timeout was specified by the user in
+# urllib3
+_Default = object()
+
+
+# Use time.monotonic if available.
+current_time = getattr(time, "monotonic", time.time)
+
+
+class Timeout(object):
+ """ Timeout configuration.
+
+ Timeouts can be defined as a default for a pool::
+
+ timeout = Timeout(connect=2.0, read=7.0)
+ http = PoolManager(timeout=timeout)
+ response = http.request('GET', 'http://example.com/')
+
+ Or per-request (which overrides the default for the pool)::
+
+ response = http.request('GET', 'http://example.com/', timeout=Timeout(10))
+
+ Timeouts can be disabled by setting all the parameters to ``None``::
+
+ no_timeout = Timeout(connect=None, read=None)
+ response = http.request('GET', 'http://example.com/, timeout=no_timeout)
+
+
+ :param total:
+ This combines the connect and read timeouts into one; the read timeout
+ will be set to the time leftover from the connect attempt. In the
+ event that both a connect timeout and a total are specified, or a read
+ timeout and a total are specified, the shorter timeout will be applied.
+
+ Defaults to None.
+
+ :type total: integer, float, or None
+
+ :param connect:
+ The maximum amount of time to wait for a connection attempt to a server
+ to succeed. Omitting the parameter will default the connect timeout to
+ the system default, probably `the global default timeout in socket.py
+ `_.
+ None will set an infinite timeout for connection attempts.
+
+ :type connect: integer, float, or None
+
+ :param read:
+ The maximum amount of time to wait between consecutive
+ read operations for a response from the server. Omitting
+ the parameter will default the read timeout to the system
+ default, probably `the global default timeout in socket.py
+ `_.
+ None will set an infinite timeout.
+
+ :type read: integer, float, or None
+
+ .. note::
+
+ Many factors can affect the total amount of time for urllib3 to return
+ an HTTP response.
+
+ For example, Python's DNS resolver does not obey the timeout specified
+ on the socket. Other factors that can affect total request time include
+ high CPU load, high swap, the program running at a low priority level,
+ or other behaviors.
+
+ In addition, the read and total timeouts only measure the time between
+ read operations on the socket connecting the client and the server,
+ not the total amount of time for the request to return a complete
+ response. For most requests, the timeout is raised because the server
+ has not sent the first byte in the specified time. This is not always
+ the case; if a server streams one byte every fifteen seconds, a timeout
+ of 20 seconds will not trigger, even though the request will take
+ several minutes to complete.
+
+ If your goal is to cut off any request after a set amount of wall clock
+ time, consider having a second "watcher" thread to cut off a slow
+ request.
+ """
+
+ #: A sentinel object representing the default timeout value
+ DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT
+
+ def __init__(self, total=None, connect=_Default, read=_Default):
+ self._connect = self._validate_timeout(connect, 'connect')
+ self._read = self._validate_timeout(read, 'read')
+ self.total = self._validate_timeout(total, 'total')
+ self._start_connect = None
+
+ def __str__(self):
+ return '%s(connect=%r, read=%r, total=%r)' % (
+ type(self).__name__, self._connect, self._read, self.total)
+
+ @classmethod
+ def _validate_timeout(cls, value, name):
+ """ Check that a timeout attribute is valid.
+
+ :param value: The timeout value to validate
+ :param name: The name of the timeout attribute to validate. This is
+ used to specify in error messages.
+ :return: The validated and casted version of the given value.
+ :raises ValueError: If it is a numeric value less than or equal to
+ zero, or the type is not an integer, float, or None.
+ """
+ if value is _Default:
+ return cls.DEFAULT_TIMEOUT
+
+ if value is None or value is cls.DEFAULT_TIMEOUT:
+ return value
+
+ if isinstance(value, bool):
+ raise ValueError("Timeout cannot be a boolean value. It must "
+ "be an int, float or None.")
+ try:
+ float(value)
+ except (TypeError, ValueError):
+ raise ValueError("Timeout value %s was %s, but it must be an "
+ "int, float or None." % (name, value))
+
+ try:
+ if value <= 0:
+ raise ValueError("Attempted to set %s timeout to %s, but the "
+ "timeout cannot be set to a value less "
+ "than or equal to 0." % (name, value))
+ except TypeError: # Python 3
+ raise ValueError("Timeout value %s was %s, but it must be an "
+ "int, float or None." % (name, value))
+
+ return value
+
+ @classmethod
+ def from_float(cls, timeout):
+ """ Create a new Timeout from a legacy timeout value.
+
+ The timeout value used by httplib.py sets the same timeout on the
+ connect(), and recv() socket requests. This creates a :class:`Timeout`
+ object that sets the individual timeouts to the ``timeout`` value
+ passed to this function.
+
+ :param timeout: The legacy timeout value.
+ :type timeout: integer, float, sentinel default object, or None
+ :return: Timeout object
+ :rtype: :class:`Timeout`
+ """
+ return Timeout(read=timeout, connect=timeout)
+
+ def clone(self):
+ """ Create a copy of the timeout object
+
+ Timeout properties are stored per-pool but each request needs a fresh
+ Timeout object to ensure each one has its own start/stop configured.
+
+ :return: a copy of the timeout object
+ :rtype: :class:`Timeout`
+ """
+ # We can't use copy.deepcopy because that will also create a new object
+ # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to
+ # detect the user default.
+ return Timeout(connect=self._connect, read=self._read,
+ total=self.total)
+
+ def start_connect(self):
+ """ Start the timeout clock, used during a connect() attempt
+
+ :raises urllib3.exceptions.TimeoutStateError: if you attempt
+ to start a timer that has been started already.
+ """
+ if self._start_connect is not None:
+ raise TimeoutStateError("Timeout timer has already been started.")
+ self._start_connect = current_time()
+ return self._start_connect
+
+ def get_connect_duration(self):
+ """ Gets the time elapsed since the call to :meth:`start_connect`.
+
+ :return: Elapsed time.
+ :rtype: float
+ :raises urllib3.exceptions.TimeoutStateError: if you attempt
+ to get duration for a timer that hasn't been started.
+ """
+ if self._start_connect is None:
+ raise TimeoutStateError("Can't get connect duration for timer "
+ "that has not started.")
+ return current_time() - self._start_connect
+
+ @property
+ def connect_timeout(self):
+ """ Get the value to use when setting a connection timeout.
+
+ This will be a positive float or integer, the value None
+ (never timeout), or the default system timeout.
+
+ :return: Connect timeout.
+ :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
+ """
+ if self.total is None:
+ return self._connect
+
+ if self._connect is None or self._connect is self.DEFAULT_TIMEOUT:
+ return self.total
+
+ return min(self._connect, self.total)
+
+ @property
+ def read_timeout(self):
+ """ Get the value for the read timeout.
+
+ This assumes some time has elapsed in the connection timeout and
+ computes the read timeout appropriately.
+
+ If self.total is set, the read timeout is dependent on the amount of
+ time taken by the connect timeout. If the connection time has not been
+ established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be
+ raised.
+
+ :return: Value to use for the read timeout.
+ :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
+ :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect`
+ has not yet been called on this object.
+ """
+ if (self.total is not None and
+ self.total is not self.DEFAULT_TIMEOUT and
+ self._read is not None and
+ self._read is not self.DEFAULT_TIMEOUT):
+ # In case the connect timeout has not yet been established.
+ if self._start_connect is None:
+ return self._read
+ return max(0, min(self.total - self.get_connect_duration(),
+ self._read))
+ elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT:
+ return max(0, self.total - self.get_connect_duration())
+ else:
+ return self._read
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/url.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/url.py
index e51950b..61a326e 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/url.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/url.py
@@ -1,226 +1,226 @@
-from __future__ import absolute_import
-from collections import namedtuple
-
-from ..exceptions import LocationParseError
-
-
-url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment']
-
-
-class Url(namedtuple('Url', url_attrs)):
- """
- Datastructure for representing an HTTP URL. Used as a return value for
- :func:`parse_url`. Both the scheme and host are normalized as they are
- both case-insensitive according to RFC 3986.
- """
- __slots__ = ()
-
- def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None,
- query=None, fragment=None):
- if path and not path.startswith('/'):
- path = '/' + path
- if scheme:
- scheme = scheme.lower()
- if host:
- host = host.lower()
- return super(Url, cls).__new__(cls, scheme, auth, host, port, path,
- query, fragment)
-
- @property
- def hostname(self):
- """For backwards-compatibility with urlparse. We're nice like that."""
- return self.host
-
- @property
- def request_uri(self):
- """Absolute path including the query string."""
- uri = self.path or '/'
-
- if self.query is not None:
- uri += '?' + self.query
-
- return uri
-
- @property
- def netloc(self):
- """Network location including host and port"""
- if self.port:
- return '%s:%d' % (self.host, self.port)
- return self.host
-
- @property
- def url(self):
- """
- Convert self into a url
-
- This function should more or less round-trip with :func:`.parse_url`. The
- returned url may not be exactly the same as the url inputted to
- :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls
- with a blank port will have : removed).
-
- Example: ::
-
- >>> U = parse_url('http://google.com/mail/')
- >>> U.url
- 'http://google.com/mail/'
- >>> Url('http', 'username:password', 'host.com', 80,
- ... '/path', 'query', 'fragment').url
- 'http://username:password@host.com:80/path?query#fragment'
- """
- scheme, auth, host, port, path, query, fragment = self
- url = ''
-
- # We use "is not None" we want things to happen with empty strings (or 0 port)
- if scheme is not None:
- url += scheme + '://'
- if auth is not None:
- url += auth + '@'
- if host is not None:
- url += host
- if port is not None:
- url += ':' + str(port)
- if path is not None:
- url += path
- if query is not None:
- url += '?' + query
- if fragment is not None:
- url += '#' + fragment
-
- return url
-
- def __str__(self):
- return self.url
-
-
-def split_first(s, delims):
- """
- Given a string and an iterable of delimiters, split on the first found
- delimiter. Return two split parts and the matched delimiter.
-
- If not found, then the first part is the full input string.
-
- Example::
-
- >>> split_first('foo/bar?baz', '?/=')
- ('foo', 'bar?baz', '/')
- >>> split_first('foo/bar?baz', '123')
- ('foo/bar?baz', '', None)
-
- Scales linearly with number of delims. Not ideal for large number of delims.
- """
- min_idx = None
- min_delim = None
- for d in delims:
- idx = s.find(d)
- if idx < 0:
- continue
-
- if min_idx is None or idx < min_idx:
- min_idx = idx
- min_delim = d
-
- if min_idx is None or min_idx < 0:
- return s, '', None
-
- return s[:min_idx], s[min_idx + 1:], min_delim
-
-
-def parse_url(url):
- """
- Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is
- performed to parse incomplete urls. Fields not provided will be None.
-
- Partly backwards-compatible with :mod:`urlparse`.
-
- Example::
-
- >>> parse_url('http://google.com/mail/')
- Url(scheme='http', host='google.com', port=None, path='/mail/', ...)
- >>> parse_url('google.com:80')
- Url(scheme=None, host='google.com', port=80, path=None, ...)
- >>> parse_url('/foo?bar')
- Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...)
- """
-
- # While this code has overlap with stdlib's urlparse, it is much
- # simplified for our needs and less annoying.
- # Additionally, this implementations does silly things to be optimal
- # on CPython.
-
- if not url:
- # Empty
- return Url()
-
- scheme = None
- auth = None
- host = None
- port = None
- path = None
- fragment = None
- query = None
-
- # Scheme
- if '://' in url:
- scheme, url = url.split('://', 1)
-
- # Find the earliest Authority Terminator
- # (http://tools.ietf.org/html/rfc3986#section-3.2)
- url, path_, delim = split_first(url, ['/', '?', '#'])
-
- if delim:
- # Reassemble the path
- path = delim + path_
-
- # Auth
- if '@' in url:
- # Last '@' denotes end of auth part
- auth, url = url.rsplit('@', 1)
-
- # IPv6
- if url and url[0] == '[':
- host, url = url.split(']', 1)
- host += ']'
-
- # Port
- if ':' in url:
- _host, port = url.split(':', 1)
-
- if not host:
- host = _host
-
- if port:
- # If given, ports must be integers. No whitespace, no plus or
- # minus prefixes, no non-integer digits such as ^2 (superscript).
- if not port.isdigit():
- raise LocationParseError(url)
- try:
- port = int(port)
- except ValueError:
- raise LocationParseError(url)
- else:
- # Blank ports are cool, too. (rfc3986#section-3.2.3)
- port = None
-
- elif not host and url:
- host = url
-
- if not path:
- return Url(scheme, auth, host, port, path, query, fragment)
-
- # Fragment
- if '#' in path:
- path, fragment = path.split('#', 1)
-
- # Query
- if '?' in path:
- path, query = path.split('?', 1)
-
- return Url(scheme, auth, host, port, path, query, fragment)
-
-
-def get_host(url):
- """
- Deprecated. Use :func:`parse_url` instead.
- """
- p = parse_url(url)
- return p.scheme or 'http', p.hostname, p.port
+from __future__ import absolute_import
+from collections import namedtuple
+
+from ..exceptions import LocationParseError
+
+
+url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment']
+
+
+class Url(namedtuple('Url', url_attrs)):
+ """
+ Datastructure for representing an HTTP URL. Used as a return value for
+ :func:`parse_url`. Both the scheme and host are normalized as they are
+ both case-insensitive according to RFC 3986.
+ """
+ __slots__ = ()
+
+ def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None,
+ query=None, fragment=None):
+ if path and not path.startswith('/'):
+ path = '/' + path
+ if scheme:
+ scheme = scheme.lower()
+ if host:
+ host = host.lower()
+ return super(Url, cls).__new__(cls, scheme, auth, host, port, path,
+ query, fragment)
+
+ @property
+ def hostname(self):
+ """For backwards-compatibility with urlparse. We're nice like that."""
+ return self.host
+
+ @property
+ def request_uri(self):
+ """Absolute path including the query string."""
+ uri = self.path or '/'
+
+ if self.query is not None:
+ uri += '?' + self.query
+
+ return uri
+
+ @property
+ def netloc(self):
+ """Network location including host and port"""
+ if self.port:
+ return '%s:%d' % (self.host, self.port)
+ return self.host
+
+ @property
+ def url(self):
+ """
+ Convert self into a url
+
+ This function should more or less round-trip with :func:`.parse_url`. The
+ returned url may not be exactly the same as the url inputted to
+ :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls
+ with a blank port will have : removed).
+
+ Example: ::
+
+ >>> U = parse_url('http://google.com/mail/')
+ >>> U.url
+ 'http://google.com/mail/'
+ >>> Url('http', 'username:password', 'host.com', 80,
+ ... '/path', 'query', 'fragment').url
+ 'http://username:password@host.com:80/path?query#fragment'
+ """
+ scheme, auth, host, port, path, query, fragment = self
+ url = ''
+
+ # We use "is not None" we want things to happen with empty strings (or 0 port)
+ if scheme is not None:
+ url += scheme + '://'
+ if auth is not None:
+ url += auth + '@'
+ if host is not None:
+ url += host
+ if port is not None:
+ url += ':' + str(port)
+ if path is not None:
+ url += path
+ if query is not None:
+ url += '?' + query
+ if fragment is not None:
+ url += '#' + fragment
+
+ return url
+
+ def __str__(self):
+ return self.url
+
+
+def split_first(s, delims):
+ """
+ Given a string and an iterable of delimiters, split on the first found
+ delimiter. Return two split parts and the matched delimiter.
+
+ If not found, then the first part is the full input string.
+
+ Example::
+
+ >>> split_first('foo/bar?baz', '?/=')
+ ('foo', 'bar?baz', '/')
+ >>> split_first('foo/bar?baz', '123')
+ ('foo/bar?baz', '', None)
+
+ Scales linearly with number of delims. Not ideal for large number of delims.
+ """
+ min_idx = None
+ min_delim = None
+ for d in delims:
+ idx = s.find(d)
+ if idx < 0:
+ continue
+
+ if min_idx is None or idx < min_idx:
+ min_idx = idx
+ min_delim = d
+
+ if min_idx is None or min_idx < 0:
+ return s, '', None
+
+ return s[:min_idx], s[min_idx + 1:], min_delim
+
+
+def parse_url(url):
+ """
+ Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is
+ performed to parse incomplete urls. Fields not provided will be None.
+
+ Partly backwards-compatible with :mod:`urlparse`.
+
+ Example::
+
+ >>> parse_url('http://google.com/mail/')
+ Url(scheme='http', host='google.com', port=None, path='/mail/', ...)
+ >>> parse_url('google.com:80')
+ Url(scheme=None, host='google.com', port=80, path=None, ...)
+ >>> parse_url('/foo?bar')
+ Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...)
+ """
+
+ # While this code has overlap with stdlib's urlparse, it is much
+ # simplified for our needs and less annoying.
+ # Additionally, this implementations does silly things to be optimal
+ # on CPython.
+
+ if not url:
+ # Empty
+ return Url()
+
+ scheme = None
+ auth = None
+ host = None
+ port = None
+ path = None
+ fragment = None
+ query = None
+
+ # Scheme
+ if '://' in url:
+ scheme, url = url.split('://', 1)
+
+ # Find the earliest Authority Terminator
+ # (http://tools.ietf.org/html/rfc3986#section-3.2)
+ url, path_, delim = split_first(url, ['/', '?', '#'])
+
+ if delim:
+ # Reassemble the path
+ path = delim + path_
+
+ # Auth
+ if '@' in url:
+ # Last '@' denotes end of auth part
+ auth, url = url.rsplit('@', 1)
+
+ # IPv6
+ if url and url[0] == '[':
+ host, url = url.split(']', 1)
+ host += ']'
+
+ # Port
+ if ':' in url:
+ _host, port = url.split(':', 1)
+
+ if not host:
+ host = _host
+
+ if port:
+ # If given, ports must be integers. No whitespace, no plus or
+ # minus prefixes, no non-integer digits such as ^2 (superscript).
+ if not port.isdigit():
+ raise LocationParseError(url)
+ try:
+ port = int(port)
+ except ValueError:
+ raise LocationParseError(url)
+ else:
+ # Blank ports are cool, too. (rfc3986#section-3.2.3)
+ port = None
+
+ elif not host and url:
+ host = url
+
+ if not path:
+ return Url(scheme, auth, host, port, path, query, fragment)
+
+ # Fragment
+ if '#' in path:
+ path, fragment = path.split('#', 1)
+
+ # Query
+ if '?' in path:
+ path, query = path.split('?', 1)
+
+ return Url(scheme, auth, host, port, path, query, fragment)
+
+
+def get_host(url):
+ """
+ Deprecated. Use :func:`parse_url` instead.
+ """
+ p = parse_url(url)
+ return p.scheme or 'http', p.hostname, p.port
diff --git a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/wait.py b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/wait.py
index 46392f2..cb396e5 100644
--- a/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/wait.py
+++ b/telegramer/include/telegram/vendor/ptb_urllib3/urllib3/util/wait.py
@@ -1,40 +1,40 @@
-from .selectors import (
- HAS_SELECT,
- DefaultSelector,
- EVENT_READ,
- EVENT_WRITE
-)
-
-
-def _wait_for_io_events(socks, events, timeout=None):
- """ Waits for IO events to be available from a list of sockets
- or optionally a single socket if passed in. Returns a list of
- sockets that can be interacted with immediately. """
- if not HAS_SELECT:
- raise ValueError('Platform does not have a selector')
- if not isinstance(socks, list):
- # Probably just a single socket.
- if hasattr(socks, "fileno"):
- socks = [socks]
- # Otherwise it might be a non-list iterable.
- else:
- socks = list(socks)
- with DefaultSelector() as selector:
- for sock in socks:
- selector.register(sock, events)
- return [key[0].fileobj for key in
- selector.select(timeout) if key[1] & events]
-
-
-def wait_for_read(socks, timeout=None):
- """ Waits for reading to be available from a list of sockets
- or optionally a single socket if passed in. Returns a list of
- sockets that can be read from immediately. """
- return _wait_for_io_events(socks, EVENT_READ, timeout)
-
-
-def wait_for_write(socks, timeout=None):
- """ Waits for writing to be available from a list of sockets
- or optionally a single socket if passed in. Returns a list of
- sockets that can be written to immediately. """
- return _wait_for_io_events(socks, EVENT_WRITE, timeout)
+from .selectors import (
+ HAS_SELECT,
+ DefaultSelector,
+ EVENT_READ,
+ EVENT_WRITE
+)
+
+
+def _wait_for_io_events(socks, events, timeout=None):
+ """ Waits for IO events to be available from a list of sockets
+ or optionally a single socket if passed in. Returns a list of
+ sockets that can be interacted with immediately. """
+ if not HAS_SELECT:
+ raise ValueError('Platform does not have a selector')
+ if not isinstance(socks, list):
+ # Probably just a single socket.
+ if hasattr(socks, "fileno"):
+ socks = [socks]
+ # Otherwise it might be a non-list iterable.
+ else:
+ socks = list(socks)
+ with DefaultSelector() as selector:
+ for sock in socks:
+ selector.register(sock, events)
+ return [key[0].fileobj for key in
+ selector.select(timeout) if key[1] & events]
+
+
+def wait_for_read(socks, timeout=None):
+ """ Waits for reading to be available from a list of sockets
+ or optionally a single socket if passed in. Returns a list of
+ sockets that can be read from immediately. """
+ return _wait_for_io_events(socks, EVENT_READ, timeout)
+
+
+def wait_for_write(socks, timeout=None):
+ """ Waits for writing to be available from a list of sockets
+ or optionally a single socket if passed in. Returns a list of
+ sockets that can be written to immediately. """
+ return _wait_for_io_events(socks, EVENT_WRITE, timeout)
diff --git a/telegramer/include/telegram/version.py b/telegramer/include/telegram/version.py
index eb67f6b..f831eee 100644
--- a/telegramer/include/telegram/version.py
+++ b/telegramer/include/telegram/version.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2018
+# Copyright (C) 2015-2022
# Leandro Toledo de Souza