diff --git a/telebot/__init__.py b/telebot/__init__.py index 44119ba..ce41c6d 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -18,7 +18,7 @@ console_output_handler = logging.StreamHandler(sys.stderr) console_output_handler.setFormatter(formatter) logger.addHandler(console_output_handler) -logger.setLevel(logging.ERROR) +logger.setLevel(logging.WARNING) from telebot import apihelper, types, util from telebot.handler_backends import MemoryHandlerBackend, FileHandlerBackend diff --git a/telebot/types.py b/telebot/types.py index b2d250e..4e33277 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + try: import ujson as json except ImportError: @@ -9,6 +11,7 @@ import six from telebot import util +logger = logging.getLogger('TeleBot') class JsonSerializable(object): """ @@ -66,9 +69,9 @@ class JsonDeserializable(object): :param json_type: :return: """ - if isinstance(json_type, dict): + if util.is_dict(json_type): return json_type - elif isinstance(json_type, str): + elif util.is_string(json_type): return json.loads(json_type) else: raise ValueError("json_type should be a json dict or string.") @@ -806,13 +809,17 @@ class ReplyKeyboardRemove(JsonSerializable): class ReplyKeyboardMarkup(JsonSerializable): def __init__(self, resize_keyboard=None, one_time_keyboard=None, selective=None, row_width=3): + if row_width>12: + logger.warning('Telegram does not support reply keyboard row width over 12') + row_width=12 + self.resize_keyboard = resize_keyboard self.one_time_keyboard = one_time_keyboard self.selective = selective self.row_width = row_width self.keyboard = [] - def add(self, *args): + def add(self, *args, row_width=None): """ This function adds strings to the keyboard, while not exceeding row_width. E.g. ReplyKeyboardMarkup#add("A", "B", "C") yields the json result {keyboard: [["A"], ["B"], ["C"]]} @@ -820,22 +827,28 @@ class ReplyKeyboardMarkup(JsonSerializable): When row_width is set to 2, the following is the result of this function: {keyboard: [["A", "B"], ["C"]]} See https://core.telegram.org/bots/api#replykeyboardmarkup :param args: KeyboardButton to append to the keyboard + :param row_width: width of row + :return: self, to allow function chaining. """ - i = 1 - row = [] - for button in args: - if util.is_string(button): - row.append({'text': button}) - elif isinstance(button, bytes): - row.append({'text': button.decode('utf-8')}) - else: - row.append(button.to_dict()) - if i % self.row_width == 0: - self.keyboard.append(row) - row = [] - i += 1 - if len(row) > 0: - self.keyboard.append(row) + row_width = row_width or self.row_width + + + if row_width>12: + logger.warning('Telegram does not support reply keyboard row width over 12') + row_width=12 + + for row in util.chunks(args, row_width): + button_array = [] + for button in row: + if util.is_string(button): + button_array.append({'text': button}) + elif util.is_bytes(button): + button_array.append({'text': button.decode('utf-8')}) + else: + button_array.append(button.to_dict()) + self.keyboard.append(button_array) + + return self def row(self, *args): """ @@ -845,14 +858,8 @@ class ReplyKeyboardMarkup(JsonSerializable): :param args: strings :return: self, to allow function chaining. """ - btn_array = [] - for button in args: - if util.is_string(button): - btn_array.append({'text': button}) - else: - btn_array.append(button.to_dict()) - self.keyboard.append(btn_array) - return self + + return self.add(args, 12) def to_json(self): """ @@ -904,13 +911,17 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable): """ This object represents an inline keyboard that appears right next to the message it belongs to. - + :return: """ + if row_width>8: + logger.warning('Telegram does not support inline keyboard row width over 8') + row_width=8 + self.row_width = row_width self.keyboard = [] - def add(self, *args): + def add(self, *args, row_width=None): """ This method adds buttons to the keyboard without exceeding row_width. @@ -920,20 +931,23 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable): When row_width is set to 2, the result: {keyboard: [["A", "B"], ["C"]]} See https://core.telegram.org/bots/api#inlinekeyboardmarkup - + :param args: Array of InlineKeyboardButton to append to the keyboard + :param row_width: width of row + :return: self, to allow function chaining. """ - i = 1 - row = [] - for button in args: - row.append(button.to_dict()) - if i % self.row_width == 0: - self.keyboard.append(row) - row = [] - i += 1 - if len(row) > 0: - self.keyboard.append(row) - + row_width = row_width or self.row_width + + if row_width>8: + logger.warning('Telegram does not support inline keyboard row width over 8') + row_width=8 + + for row in util.chunks(args, row_width): + button_array = [button.to_dict() for button in row] + self.keyboard.append(button_array) + + return self + def row(self, *args): """ Adds a list of InlineKeyboardButton to the keyboard. @@ -942,13 +956,12 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable): InlineKeyboardMarkup.row("A").row("B", "C").to_json() outputs: '{keyboard: [["A"], ["B", "C"]]}' See https://core.telegram.org/bots/api#inlinekeyboardmarkup - + :param args: Array of InlineKeyboardButton to append to the keyboard :return: self, to allow function chaining. """ - button_array = [button.to_dict() for button in args] - self.keyboard.append(button_array) - return self + + return self.add(args, 8) def to_json(self): """ @@ -2291,6 +2304,9 @@ class InputMedia(Dictionaryable, JsonSerializable): class InputMediaPhoto(InputMedia): def __init__(self, media, caption=None, parse_mode=None): + if util.is_pil_image(media): + media = util.pil_image_to_file(media) + super(InputMediaPhoto, self).__init__(type="photo", media=media, caption=caption, parse_mode=parse_mode) def to_dict(self): diff --git a/telebot/util.py b/telebot/util.py index b4589d4..7612ec4 100644 --- a/telebot/util.py +++ b/telebot/util.py @@ -166,6 +166,12 @@ def async_dec(): def is_string(var): return isinstance(var, string_types) +def is_dict(var): + return isinstance(var, dict) + +def is_bytes(var): + return isinstance(var, bytes) + def is_pil_image(var): return pil_imported and isinstance(var, PIL.Image.Image) @@ -278,6 +284,11 @@ def per_thread(key, construct_value, reset=False): return getattr(thread_local, key) +def chunks(lst, n): + """Yield successive n-sized chunks from lst.""" + # https://stackoverflow.com/a/312464/9935473 + for i in range(0, len(lst), n): + yield lst[i:i + n] def generate_random_token(): return ''.join(random.sample(string.ascii_letters, 16))