Как стать автором
Обновить

Создаем библиотеку для бота telegram

Время на прочтение16 мин
Количество просмотров13K

История об изучении использовании python для написания бота библиотеки для ботов в telegram.

Код, описанием и историей которого является эта статья доступен по ссылке.

Предыстория

Как-то в разговоре со знакомым, на тему проф ориентации и общей перспективности в смысле бабла, прозвучал вопрос: «Пишешь ли ты на питоне?»

Я ответил в том смысле, что на любых животных писать неудобно. Они либо кусаются, либо убегают, а часто одновременно и от этого адски страдает каллиграфия. А на круглых, к которым относится питон, как самоходный шланг, и пытаться-то глупо.

Оказалось, что речь шла не о хладнокровной, а об электронной сущности, в смысле языка программирования: «Знаешь о таком?»

Разумеется, я знал и, за следующие 15 секунд, я вывалил на собеседника все мои познания о том, что это очередной Н-плюс-первый птичий язык, на котором пишет довольно много народу, правда не особо понятно что именно и зачем, однако в моей жизни его необходимость в нем равна приблизительно нулю. «Хот нет, вру. Пацан для колледжа делал какую-то домашку именно на питоне и просил помочь там с чем-то. Писал какого-то «бота» для телеграмма.»

В ответ я узнал, что именно питон и именно телеграмм - это крайне модно, молодёжно, современно и вообще: бабы кончают, дети смеются.

- Ты хотя бы пробовал?

- Ну, это же телеграмм ставить надо… Это же какой-то чат в телефоне, правильно?

- Ясно с тобой все.

Рюмка чая, вместе с нашим с ним разговором, подошли к концу, но идея запала. Тем более, что у меня как раз была неделя, в которую я был практически абсолютно свободен аж до следующего понедельника. «Аж кончают… да ну нафик, брехня. Но глянуть можно… Вай бы и не нот, так сказать?».

Начало

Чайник вскипел, чай налит, комп гудит: «Ну-с, и чего там оно этот питон?… Эй Влад, та на чем писал на этой фигне? Бесплатная? Ага качается. А писал чего? Бота? Ясно».

О самом питоне мне сказать особо нечего. В процессе поиска я наткнулся на фразу: «На python написано самое большое количество кода в мире», с которой теперь, спустя пару недель я бы поспорил.

Про "накрутку" для статистики

Если выкинуть из текста программ на питоне начальные пробелы и клепанный "self.", то рейтинг количества кода можно смело делить вдвое.

Ну да пусть она остается на совести создателя статьи, к делу это не относится.

В этой статье я не буду описывать ни как скачать PyCharm или десктопную версию телеграмма (зарегистрироваться в которой оказалось той еще проблемой, кстати), ни как создать своего бота и получить API Key. Этой 30-секундной информацией интернет просто завален. Будем подразумевать, что потенциальному читателю это либо вообще до фени, либо он способен эти операции произвести самостоятельно.

Переходим к сути

А суть в том, что после кое-какой настройки среда для написания кода готова, найдена и скачана библиотека для написания ботов в телеграмм, бегло просмотрены ее примеры, пастнуты в редактор кода и вот первый бот уже умеет говорить: «Я увидел: ‘ваш текст’».

В качестве библиотеки я выбрал «aiogram». Просто потому, что первые две попавшиеся были какие-то вообще крайне печально документированные. Ну да «опенсорс–же—блин», так что удивлен я не был, просто искал что-то следующее, где хотя бы примеры были на часть функционала. О своем выборе библиотеки не жалею. Она успешно работает, все свои функции выполняет без нареканий. Другое дело, что 60-80% ее функционала мне не пригодилось, но об этом будет чуть позже.

В принципе, можно использовать вообще любую библиотеку, реализующую АПИ телеграмма. Все они идентичны до синтаксиса, отличаясь нюансами наполнения.

Кратко о функционировании ботов для телеграмм

Бот – это http клиент, который посылает серверу телеграмм команды, получает в ответ результат их выполнения и, параллельно, события, которые произошли в области интереса бота с момента последнего запроса. Сам бот может быть запущен где угодно, лишь бы с того утюга, на котором он функционирует, был доступ к сайту телеграмма. Больше требований к месту его расположения никаких нет.

Это удобство.

Неудобство заключается в клиент-серверной идеологии самого АПИ. Реализация АПИ нас не волнует вообще, т.к. мы общаемся уже через библиотеку прокладку, но нас волнует событийность модели. Все библиотеки для работы с телеграмом которые я видел реализованы абсолютно одинаково:

  1. есть бесконечный цикл запросов на получение новых событий от сервера

  2. при получении событий дергаются куски пользовательского кода

Пользовательский код можно описывать с разной степенью «задекорированности», но так или иначе это всегда должен быть какой-то конечный кусок, реализующий действия по каждому элементарному событию. Учитывая то, что основных события всего два: текст сообщения и нажатие на кнопку в сообщении, то в пользовательской части весь код, по сути, состоит из вызова всего двух функций.

В этом заключается основная проблема т.к. реализовать сколько-нибудь сложную логику на двух асинхронно вызываемых функциях, дергаемых по каждому событию крайне сложно. Разумеется, я ознакомился с «типовыми» подходами по написанию кода в этих условиях. Я знаю о наличии декораторов с кучей условий, а так же хорошо знаком с концепцией конечного автомата.

Возможно из-за того, что я некоторые решения представляю себе довольно хорошо, я еще на берегу понял, что это тупиковый путь. Реализовать бота на событийной модели можно, но для этого нужно четкое описание логики «высеченное в граните». У меня же нет четко поставленной задачи по написанию чего-то конкретного, а титанизм усилий при попытке заменить на половине дороги логику обработки или используемый инструментарий я представлял замечательно, поэтому я даже пробовать не стал делать что-то сложнее примеров для ознакомления.

Второй «печальный» момент, с которым придется столкнуться при написании ботов – это, хм… невменяемость протокола. Прямо по нему видно, что его писали кусками в разное время и при добавлении чего-то нового ничего из старого никогда не менялось. В результате в протоколе нет практически ни одной сходной вещи, которые бы функционировали одинаково. Пара примеров. Есть два типа клавиатур, которые можно привязать к сообщению: INLINE и KEYBOARD.

Кроме слова «клавиатура» них нет ничего общего. Кнопки у них описываются по разному, имеют разный набор полей и даже сообщения с этими клавиатурами ведут себя различно и несовместимо. Сообщения с картинкой и типом «photo» – это отдельная сущность, которая с другими сообщениями имеет мало общего. У него даже текст подписи называется «caption» вместо «text». Сообщения с другими «вложениями» - это опять отдельные сущности.

В общем, попытка унифицировать этот винегрет обещает длительную и плодотворную работу методом тыка. Не очень удивительно то, что основные библиотеки для ботов даже не пытаются эту работу делать. Впрочем, к текущей статье это имеет теоретическое отношение.

Итого, задача

После того, как мой этап знакомства с АПИ телеграмма и используемой библиотеки привел меня к знаниям, описанным в паре прошлых абзацев, я отложил попытки написать что-то цельное и начал искать готовые решения по «очеловечиванию» реализации «бизнес логики» бота.

К огромному моему удивлению я не нашел вообще ничего. Я сейчас имею в виду не вебовские «конструкторы» ботов, коих миллион (ну или приблизительно столько), у меня все же стояла задача по изучению питона, а не по тренировке мелкой моторики правой руки компьютерной мышью. Я имею в виду какие-то библиотеки, которые бы позволяли реализовывать логику работы конечного продукта (бота) в привычной линейной форме, полностью убирая все особенности модели общения с сервером с глаз пользователя. Не нашел.

В результате, после довольно краткого знакомства с «полем боя» задача плавно трансформировалась из «пишем бота» в «пишем средство для написания бота». Мне, в сущности, это было даже гораздо интереснее и ближе т.к., повторюсь, основной задачей было именно освоение питона, а не реализация конкретной задачи, но все же было бы интересно узнать, если такие решения существуют.

Итого: необходимо реализовать средство, которое бы позволяло писать ботов для телеграмма в привычной линейной схеме реализации алгоритма. Т.е. чтобы можно было писать что-то типа такого (питоно–подобный псевдокод):

Желаемый синтаксис логики бота
def logic( chat ):
    cfg = getSomeChatPersistentConfig()
    name = chat.user.name

    say( 'Hi, im a bot' )
    if ( ask( 'I see you name as {name}\nCan I use it talking with you?' ) )
      cfg.name = name
    else
    while True:
      name = input('Please enter you name')
      if name:
        cfg.name = name
      else
        match choise('Your cancelled, what do you nant to do?'):
          case 'QUIT':
             say( 'Well, its sad, but hope you back soon.'
             return

          case 'NONEW': 
             cfg.name = user.name
             say( 'Ok, let it be, {cfg.name}' )
             break

          case 'RETRY': continue

    chat.clear()
    say( 'Lets start to work {cfg.name}!' )

    while True:
      match menu('What do you wana do?'):
        case 'QUIT':     break
        case 'DEVICES':  CallDeviceDisplayProc()
        case 'SETTINGS': CallSettingsProc()

     say('Bye, see you')

Т.е. решать задачу, а не воевать с ограничениями, накладываемыми на способ ее реализации.

Для решения этой задачи нам нужно сначала понять пару простых вещей:

  1. Логика: Какие интерфейсы нам нужны для реализации интерактивных задач.

  2. Функции бота: Какие способы взаимодействия с пользователем существуют, как и какие нужно использовать, чтобы реализовать нужное для нашей логики.

  3. Работа бота: Как и где эксплуатировать нашего бота. Как он будет получать управление.

  4. Запуск: Как запустить нашу логику параллельно «бесконечному циклу опроса сервера».

Работа бота

Я не буду вдаваться в различия способов эксплуатации бота и возможных мест откуда он может быть вызван, что и где можно или нельзя использовать. В конце концов у нас не бесконечность, а всего лишь одна неделя на то, чтобы ознакомиться с питоном и создать какой-то конечный продукт, поэтому сосредоточимся только на персональных чатах (тех, когда человек пишет чату явно, запуская его кнопкой «запуск»). По сходной же логике работают и боты, которые были добавлены в групповые каналы, но там есть свои особенности. Будем писать код, не обращая внимания на эти различия. По крайней мере пока.

Итого: пишем библиотеку для ботов, ориентированных на работу в персональном канале.

Функции бота

При работе телеграмма существуют следующие способы взаимодействия бот-человек:

  • Написание текста как с одной, так и с другой стороны – это сообщение. В этом важно то, что бот ничего не делает до тех пор, пока ему не придет первое сообщение. Именно при получении сообщения определяется, откуда его написали (из какого канала), и кто это сделал. Т.к. события могут происходить абсолютно не связанно друг с другом, то сообщения будут приходить для разных каналов, от разных людей, которые находятся на разном этапе общения с ботом. Учитывая это нужно понимать что одна линейная бизнес-логика должна работать только для одного канала. Или так: для каждого канала должна быть запущена своя уникальная копия бизнес-логики. Сохранение как промежуточных, так и финальных данных происходит так же на «по–канальной» основе.

  • В сообщении формируемом ботом, помимо «контента» можно использовать кнопки. Кнопки бывают двух видов: привязанные к сообщению и к чату (хотя тоже к сообщению, но зрительно… блин, тьфу на тебя АПИ телеграмма!). В детальное описание кнопок и их различий я вдаваться не буду по описанному уже выше принципу: «кому пофиг, а кто и так в курсе». В контексте статьи важен сам факт и различия в использовании этих кнопок. Кнопки у чата просто спамят текст в канал, а те, которые у сообщений, вызывают отправку специального обновления.

Подводим итог.

Бот может получить управление в любой момент, когда пользователь напишет какое-то сообщение или нажмет на кнопку. Это может случиться через секунду или через неделю. Не то, чтобы это было как-то важно для нашего недельного марафона, но учитывать все же стоит.

Нарисуем что-то типа:

class ISettings(typing.Sized):
    """Interface for ``Settings`` class"""
    _cfg: 'ISettings'

    def __init__(self,cfg:'ISettings'):
        self._cfg = cfg

    def gopt(self, path: str, default: TSettingsOption_t) -> TSettingsOption_t:
        """Get data from settings or set it to default if data not found"""
        return self._cfg.gopt(path,default)

    def sopt(self, path: str, value: TSettingsOption_t) -> TSettingsOption_t:
        """Set settings option to value """
        return self._cfg.sopt(path,value)

    def sub_cfg(self, nm: str) ->'ISettings':
        """Get settings sub-key by full path in form of 'key.key'... """
        return self._cfg.sub_cfg(nm)

и

class SettingsIStorage:
    """Interface for load\save storage"""
    def load(self, settings: ISettings):
        pass

    def save(self, settings: ISettings):
        pass

и забудем об этом вопросе.

Общение с ботом происходит в каждом канале независимо и уникально. Учитывая еще и тот факт, что боту может поступать сообщение с произвольными интервалами, то логика обработки сообщений должны быть такой: при получении сообщения находим канал, который его обслуживает.  Если канала нет, то создаем новый. Передаем каналу сообщение. Если в канале уже работает логика, то распределяем сообщение в соответствии с ее потребностями, если ее нет, то запускаем ее, передав сообщение как стартовое. Код приводить не буду т.к. в реализации это все крайне просто, но очень много пробелов , «self.» и прочего не важного текста.

Желающие могут посмотреть реализацию самостоятельно.

Логика

Накидаем пример того, как нам хотелось бы, чтобы наша программа выглядела.

Например, так:

Пример рабочей программы
class Logic(ILogic):
    async def main(self, chat: BotChat, params: str) -> None:
        chat.user().name = chat.last.from_user.full_name
        name = chat.user().name

        if params:
            pstr = f'\nYou started me with parameters *"{escape_md(params)}"*, but I dont support any ?\n\n'
        else:
            pstr = ''

        titleMsg = await chat.reply(
            f'Hi, *{name}*.\n'
            f'{pstr}'
            f'You are at examples section',
            media='data/Icon-Hi.png'
        )

        while True:
            rc = await chat.menu(
                'Choose test group to go',
                [[('➡ Menu tests...', 'menu')],
                 [('❓ Some asking', 'ask'), ('✌ Funny one :)', 'wait'), ('?', 'calc')],
                 [('❌ Close', 0), ('❌ Cancel', 0), ('❎ Abandon!', 0), ('➰ F* off!!', 0)],
                 ],
                remove_unused=True
            )
            if not rc.known: break
            if rc.data == 'menu':
                await logic_MENU(chat, name)
            elif rc.data == 'ask':
                await logic_ASK(chat, name)
            elif rc.data == 'wait':
                await logic_WAIT(chat, name)
            elif rc.data == 'calc':
                await logic_CALC(chat, name)
            else:
                break

        await titleMsg.delete()
        await chat.say(f'Calm down mate!\nIts all done already.\nSee you ?', wait_delay=1)
        await chat.say(f'...btw, if you wanna reply you can use "/start" command.', wait_delay=2)
        await chat.say(f'Just saying...')

На примере этого кода видно, что нам нужно реализовать:

  • Передачу сообщений (в идеале с картинками).

  • Удаление/Изменение сообщений

  • Все вышеперечисленное, с учетом того, что нужны кнопки управления, которых бывает 2 вида.

  • Реализовать «модальность» для сообщений, т.е. когда все события обрабатываются только одним элементом монопольно не давая выполняться обработчикам из предыдущих элементов до тех пор, пока модальный элемент не будет закрыт. Это нужно для реализации ввода, ответов на вопросы, меню и прочего.

  • Реализовать возможность обрабатывать нажатия на кнопки в немодальных сообщениях.

Запуск

Любые библиотеки для работы ботов представляют из себя, в конце, концов такое:

executor.start_polling(dp)

Т.е. запуск некоего бесконечного цикла опроса и реакций на события, в то время как пользовательский код вызывается разными сортами такого:

@dp.channel_post_handler()
async def post_handler(message: Message_t):
    …
@dp.message_handler()
async def message_handler(message: Message_t):
    …
@dp.callback_query_handler()
async def callback_handler(cbd: types.CallbackQuery):
    …

Т.е. наш код будет вызван при получении различных событий. Их есть некоторое разнообразие, но мы сосредоточимся на простейших: получение нового сообщения в канале и получения уведомления при нажатии кнопки.

Внедриться внутрь цикла с линейной логикой не представляется возможным, поэтому нужно запустить нашу программу параллельно ей и обеспечить взаимодействие из нее с данными, получаемыми «мелкими порциями» асинхронно из основного цикла.

Мне, как человеку далекому от птичьих языков более привычным был бы способ создания отдельного процессорного потока для выполнения логики, но раз подвернулась такая оказия, то заодно разберемся с co-routines (я без понятия как это называется по русски), в windows эти сущности называются Fibers, где-то еще могут быть другие названия.

Суть подхода в том, что множество логически не связанного кода выполняется в одном потоке, передавая друг другу управления в тот момент, когда этому коду нечего делать.

Мы пишем http клиента и «нечего делать» - это его основное занятие, поэтому в топку «взрослые» потоки, пишем на «резиновых женщинах».

Добавить свой код в очередь выполнения очень просто:

        async def _wrapper():
            self.log.error(f'Start bot logic task')
            try:
                await self.logic.main(self, params if params else '')
                self.logicTask = None
								…
        self.logicTask = asyncio.get_event_loop().create_task(_wrapper())

где logicTask хранит объект добавленного кода, а self.logic.main запускает на выполнение процедуру с реализацией линейной логики. С учетом разных нужд пусть интерфейс для нашей логики будет выглядеть так:

Код интерфейса логики
class ILogic(ISettings):
    """Interface for linear logic implementation for each ``BotChat`` channel.

    Each ``BotChat`` object uses this interface to call user logic
    functions. Each channel will have separate, unique logic object.
    """
    chat: 'BotChat'

    def __init__(self, chat: 'BotChat', cfg: ISettings):
        super().__init__(cfg)
        self.session = chat

    async def main(self,chat:'BotChat',params:typing.List[str]) -> None:
        """Main user logic function. Called after '/start' or '/restart' command and will run until
        exception happen or finished.

        Executed in parallel with bot task.

        Note: MUST NOT block on operations called from outside (like callbacks).

        :param chat: parent chat executing logic
        :param params: string with parameters passed to '/start' or '/restart' commands.
        """
        pass

    def OnExit(self,chat:'BotChat',isAlive:bool) -> None:
        """Called after logic procedure finished.
        Used just for notification. Can be used f.i. to free resources.

        :param chat: parent chat executing logic
        :param isAlive: True is chat object is alive (can be used to communicate with channel), False if chat is closed.
        """
        pass

    # ret false to disable bot_ilogic restart
    async def OnDownDecide(self, chat: 'BotChat', message: Message_t) -> bool:
        """Called to decide what to do if some notifications received in channel but logic is down (finished or terminated by error)

        :param chat: parent chat executing logic
        :param message: message object which "wake up" channel
        :return: True to restart logic task or False to stay dead.
        """
        return True

Пользователь нашей библиотеки будет наследоваться от этого интерфейса и реализовывать то, что ему нужно. По крайней мере main.

Код

Разбираемся с АПИ телеграмма (в синтаксисе выбранной библиотеки) и выясняем что все кроме модальности реализуемо довольно просто:

Создаем новые сообщения (тип зависит от того с картинкой оно нужно или нет) со всеми возможными наворотами:

    async def _createMessage(self) -> MessageId_t:
        reply_to_message_id = self.reply_to_message_id
        if not reply_to_message_id: reply_to_message_id = None
        if self.media:
            msg = await self.chat.bot.send_photo(
                self.chat.chat_id, parse_mode=self.chat.bot.parse_mode,
                photo=self._loadMedia(self.media), caption=self.chat.escape_soft(self.text),
                reply_markup=self.keyboard.markup,
                reply_to_message_id=reply_to_message_id)
        else:
            msg = await self.chat.bot.send_message(
                self.chat.chat_id,
                text=self.chat.escape_soft(self.text), reply_markup=self.keyboard.markup,
                reply_to_message_id=reply_to_message_id)
        return msg.message_id

Редактирование существующего сообщения будет чуть сложнее (спасибо такому однозначному и «единообразному» протоколу телеграмма), но тоже ничего выдающегося:

async def _updateMessage(self) -> None:
  if self.media:
    if self._media.changed:
      await self.chat.bot.edit_message_media(
        media=types.InputMedia(
          type='photo',
          media=self._loadMedia(self.media),
          caption=self.chat.escape_soft(self.text)
        ),
        chat_id=self.chat.chat_id, message_id=self.message_id,
        reply_markup=self.keyboard.markup)
		elif self._text.changed:
        await self.chat.bot.edit_message_caption(
          chat_id=self.chat.chat_id, message_id=self.message_id,
          caption=self.chat.escape_soft(self.text), reply_markup=self.keyboard.markup
        )
	else:
		if self._text.changed:
      await self.chat.bot.edit_message_text(
        text=self.chat.escape_soft(self.text),
        chat_id=self.chat.chat_id, message_id=self.message_id,
        reply_markup=self.keyboard.markup
      )
    elif self.keyboard.changed:
      try:
        await self.chat.bot.edit_message_reply_markup(
          chat_id=self.chat.chat_id, message_id=self.message_id,
          reply_markup=self.keyboard.markup
        )
        # just mask unchanged error instead complex keyboard comparison
      except aiogram.utils.exceptions.MessageNotModified:
        pass

Это все, разумеется, надо обернуть кучей логики и проверок на то, в каких условиях и с какими «бубнами» вызывать то или другое. Желающие могут посмотреть реализацию самостоятельно.

Удаление сообщений существует и, слава богу, простейшее без каких-либо ветвлений в истории развития протокола:

try:
  if await self.bot.delete_message(chat_id=self.chat_id, message_id=message_id):
    return True
  except BadRequest as e:
    return False

Вроде все.

Кнопки в протоколе описываются по разному, но пофик, унифицируем все до:

BotUserKey_t = typing.Union[typing.Tuple[str, typing.Any], str]

для любой клавиатуры. Это лишняя страница кода, зато не нужно париться с типами при использовании. Стоит того.

Модальность к самому телеграмму отношения уже не имеет - это чисто логический наворот, который реализуем простой очередью сущностей, которые ждут получения событий. Т.к. событий всего два, то и интерфейс для такой сущности будет иметь всего две полезные функции. Так же эти «ждуны» должны уметь быть посредниками между кодом логики, который будет ожидать каких-то событий и получаемыми мелкими событиями. Принцип простой: с одной стороны, если логике что-то надо, то она заводит «ждуна» и ждет, пока поставленное условие не выполнится. С другой стороны, при получении мелких событий от телеграмма, проверяется очередь «ждунов» и, начиная с последнего, полученные данные применяются до тех пор, пока не дойдем до модального «ждуна» или до такого, который ровно этого события и ждал. Реализуется почти так же просто как и звучит.

Интерфейс ждуна, грубо говоря, такой:

Интерфейс класса ожидания
class Waiter:
    """Class which is used to filter received messages and callbacks and pass execution to
    user logic.
    """
    chat: 'BotChat'
    isModal: bool = False

    def __init__(self, chat: 'BotChat', messge_id: MessageId_t, /,
                 on_message: typing.Optional[OnMessageEvent] = None,
                 on_callback: typing.Optional[OnCallbackEvent] = None):
        """ Create base waiter class

        :param chat: parent chat waiter will be attached to
        :param messge_id: message id this waiter attacked to if applicable (NoMessageId if waiter not attached to single message)
        :param on_message: Callback to call on new messages
        :param on_callback: Callback called on new INLINE buttons data
        """
        self.chat = chat
        self.messge_id = messge_id
        self._completed = asyncio.Event()
        self._completed.clear()
        self._on_message = on_message
        self._on_callback = on_callback

    async def isWaitingThisMessage(self, chat: 'BotChat', message: Message_t) -> bool:
        """Check if this waiter process specified message"""
        if self._on_message and await self._on_message(self.chat, message):
            return True
        return False

    async def isWaitingThisCallback(self, chat: 'BotChat', cbd: Callback_t) -> bool:
        """Check if this waiter process specified callback data"""
        if self._on_callback and await self._on_callback(self.chat, cbd):
            return True
        return False

    def notify_complete(self):
        """Used to notify waiting user logic, what wait is complete. Called from bot loop to inform user logic"""
        self._completed.set()

    async def wait(self, timeout: float = None) -> bool:
        """Wait until complete. Called from user logic to wait waiter condition."""
        if timeout and timeout >= 0:
            try:
                await asyncio.wait_for(self._completed.wait(), timeout)
                return True
            except asyncio.TimeoutError:
                self.chat.waiterRemove(self)
                return False
        else:
            await self._completed.wait()
            return True

Ну и в классе нужно не забыть создать для него небольшую инфраструктуру:

waitersLock: threading.RLock
waiters: typing.List[typing.Optional[Waiter]]

В этом месте есть небольшой момент, на который стоит обратить внимание. Локер, на котором синхронизируется список «ждунов» должен быть: а) реентерабельным и б) не отпускающим очередь «детских потоков».

Первое понятно для чего, а вот второе важно в единственном моменте. В телеграмме, при получении события от нажатой кнопки на это событие можно ответить и при ответе у пользователя всплывет либо модальное окошко с текстом, либо пропадающий хинт. Так вот, если использовать блокировки из набора асинхронной библиотеки, то в момент блокировки будет передано управление первому попавшему ждущему своей очереди коду. Если окажется так, что этот код отправит серверу телеграмм, какие-то данные , то ответ на событие уже можно и не отправлять – оно не сработает, пользователь не увидит реакции.

Если не хочется лишать себя такой функции как эти «быстрые ответы прям сразу при нажатии», то асинхронные локеры использовать нельзя. В примере выше используется многопоточный реентерабельный локер, хотя учитывая структуру самого приложения его функция чисто декоративная и не более интеллектуальная чем:

While not stateVariable:
	pass

только что использовать удобнее и писать меньше.

По функционалу вроде бы все.

Итог

Если не лукавить, то времени ушло чуть более недели. Впрочем, меня извиняет поездка на шашлыки и пара катаний на дачу.

Зачем я все это тут описал?

Целей у меня, на самом деле, несколько.

Во-первых, хотелось бы узнать мнение тех, кто пишет на питоне давно и серьезно. Все же этот код я писал как знакомство я языком и буду рад любым отзывам о его качестве. Однако просьба не забывать, что 2 недели назад я путал питона с ужом :)

Во-вторых, мне интересно было бы узнать о подобных альтернативах. Все же я не верю, что такое никто не реализовывал ранее.

Ну и, в третьих, на тот случай, если мой код или его описание окажется для кого-то в чем-то полезным. Буду рад.

Код, описанием и историей которого является эта статья доступен по ссылке.

Теги:
Хабы:
Всего голосов 12: ↑4 и ↓8-2
Комментарии25

Публикации

Истории

Работа

Data Scientist
94 вакансии
Python разработчик
199 вакансий

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн