Русский
Русский
English
Статистика
Реклама

Телеграм-бот

Чем опасен ТГ-бот, позволяющий подменять Caller-ID

04.02.2021 14:12:29 | Автор: admin

Сегодня в СМИ ворвалась новость про бот в Telegram для телефонных звонков с функцией подмены обратного номера. На Хабре тоже уже появилась. Ну а так как я и есть Алексей Дрозд, подумал, что вам может быть интересно узнать немного подробностей про функционал бота и про угрозы, которые он несёт.

Старая песня на новый лад

С технической точки зрения в атаке ничего нового. Новизна, скорее, с функциональной стороны. Появление такого бота снижает до нуля порог вхождения для мошенников. Если раньше надо было немного пошевелить мозгом для того, чтобы прочитать инструкции по настройке виртуальной АТС, то сейчас достаточно шевелить лишь пальцем. Всё интуитивно понятно.

Удешевление атаки, равно как и снижение порога входа, способствует росту популярности. Первой ассоциацией, пришедшей на ум, был давний пост @LukaSafonovпро утёкшие исходники Citadel. Спрос порождает не только предложение, но и сервис. Вот и у ТГ-бота тоже есть возможность покурить мануалы, написать в сапорт и посмотреть различные отчёты своей деятельности. В лучших традициях, прикручена партнёрка с рефералами.

Что касается тарифов, то всё более-менее стандартно. Посекундная тарификация и различные пакеты.

С чего вдруг ополчился на бот?

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

Зачем легитимному сотруднику колл-центра такой функционал? Вопрос из разряда риторических. Кроме того, даже без тестового звонка можно предположить, что бот изменяет pitch. А это обратимый процесс, если записать голос звонившего.

В чём опасность?

В первую очередь, как уже писал, увеличение потока желающих. Классическая атака состоит из 2 частей: позвонить-напугать-убедить + вывести деньги. Часть 1 стало легче организовать. Часть 2, в принципе, отработана и так.

Во вторую очередь меняются подходы. Если раньше преимущественно звонки шли от "сотрудников службы безопасности банка", то в последнее время чаще наблюдаю двухходовку. Сперва мошенники узнают реальные номера отделений полиции, ФИО работников. Далее, звонят жертве с подменного номера, представляясь сотрудником ведомства, и просто предупреждают о зафиксированной попытке списания средств. Никаких действий совершать не просят. Всячески разыгрывают спектакль "Моя милиция меня бережёт". В конце разговора предупреждают о том, что скоро свяжется сотрудник банка и нужно будет следовать его инструкциям. Собственно, ради этого напутствия спектакль и затевался.

После разговора с "полицейским" некоторые жертвы проявляют здравый скепсис и лезут в Сеть проверять достоверность предоставленной информации. Естественно, находят и номер, и ФИО сотрудника, от имени которого вёлся разговор. It's a trap! Подозрительность снижается и звонящему банковскому сотруднику верят охотнее.

P.S. Не стоит надеяться, что единожды проинформировав родственников и знакомых о мошеннической схеме, вы решите проблему навсегда. Увы, практика показывает, что в подавляющем большинстве случаев жертвы до последнего уверены, что с ними такого не случится. Поэтому настоятельно рекомендую к прочтению пост @iiwabor. А самые отчаянные могут найти бота и воспроизвести атаку на своих же. Бесплатного времени хватит на несколько секунд. Но, надеюсь, у вышей "жертвы" наступит прозрение, когда вы покажете наглядно, как легко "на коленке" можно организовать атаку. Пусть запомнят раз и навсегда, им звонить никто не будет. Если есть сомнения, надо повесить трубку и самостоятельно связаться с банком\отделом полиции\прокурором\спортлото.

Подробнее..

Из песочницы Бот Умный планировщик понимает с полуслова

29.07.2020 14:20:10 | Автор: admin
Если вы когда-нибудь желали иметь личного слугу, который бы напоминал вам обо всем, о чем вы ему скажите, но не имели возможности нанять такого, то разработанный мною бот станет ему достойной заменой.



Хотите проверить функционал? Напишите в лс боту по этой ссылке и он ответит вам.

А тем, кому интересно как он работает и как 16-летний школьник смог написать его, я с удовольствием расскажу всё в подробностях в этой статье.

Предыстория


Началось все с другого моего бота для вк на node js, который расшифровывал голосовые сообщения (и так как таких ботов пруд пруди, я решил не писать статью о его разработке, хотя и он имеет несколько отличительных особенностей). После того, как я запустил его, мой отец предложил мне разработать другого бота для нашего семейного чата в телеграмме. Суть этого бота была простой извлекать дату и время из текстового сообщения человека, и, используя телеграмовские отложенные сообщения, создавать напоминания прямо в семейном чате.

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

Разработка


Общие сведения


Этот бот написан на node js и живет на heroku.

Он способен хранить любые текстовые напоминания с точностью до минуты.

Также он может работать в групповых беседах.

На данный момент бот распознает дату и время только на русском языке.

SmartScheduler open source проект, доступный на моем гитхабе.

Извлечение даты и времени из сообщения


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

Была создана большая база данных (constValues.js), хранящая в себе константные выражения и их свойства, которые человек использует в своей речи. Затем были написаны функции для распознавания каждого варианта написания времени.

К примеру для распознавания даты в виде через X %тип_времени% используется функция FindAdditiveLiterals, а для поиска дня недели FindDayOfWeek.

Для каждого варианта обозначения времени выставлялся свой приоритет.

В итоге алгоритм работы парсера выглядит следующим образом:

  1. Исходная строка делится на слова. Слов, в которых производится поиск времени, не может быть больше 40.
  2. Массив слов пропускается через функцию конвертации слов в числа.
  3. Находятся все указания времени в сообщении, а также помечаются использованные в указаниях слова (например в указании будильник 8 часов отмечаются слова 8 и часов).
  4. Если какая-то характеристика времени не была найдена (например месяц) в текстовом сообщении, то берется текущее значение этой характеристики.
  5. Для окончательного вердикта выбираются указания времени с наибольшим приоритетом и смежные указания, имеющие одинаковое исходное слово (например в слове 10:30 одновременно указан и час, и минута).
  6. После выбора окончательных характеристик времени формируется штамп времени из выбранных минуты, часа, дня, месяца и года.
  7. Из массива слов удаляются все помеченные слова, а из оставшихся формируется текст напоминания.
  8. Если сформированный штамп времени больше текущего времени, то мы считаем что такое напоминание пригодно и функция возвращает объект типа
    { string: answer, string: text, date: date }

    В противном случае функция возвращает объект
    { string: answer, string: text }
    (answer ответ для пользователя, text текст напоминания, date дата напоминания).

База данных напоминаний


Следующими вопросами были где хранить все напоминания и как следить за их выполнением.

Изначально я хотел воспользоваться библиотекой node-schedule, но отказался от этой идеи, так как я не хотел засорять оперативную память всеми напоминаниями.

Вместо этого я решил изучить принцип работы SQL баз данных и создать свою.

После прочтения множество статей в интернете я создал postgresql базу данных на платформе heroku, так как в ней можно встраивать бдшки в свои приложения, что очень удобно.

Чтобы взаимодействовать с бд я написал небольшой скрипт (db.js), в котором реализовал все необходимые функции, такие как инициализация бд, получение списка напоминаний и т.д.

В моей базе данных присутствует две таблицы: первая для хранения напоминаний, вторая для хранения часовых зон пользователей (о ней чуть позже).

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

Также я добавил отображение всех напоминаний через команду /list.



(рядом с каждым напоминанием есть кликабельная команда /N, которая удаляет его при клике на неё)

Настройка часового пояса


До того, как я решил написать эту статью и выложить её на Хабр, в переменных среды был захардкожен часовой пояс Москвы. Для пользования внутри нашей семьи этого было достаточно, но для того, чтобы воспользоваться всеми прелестями и удобствами жизни с ботом SmartScheduler мог каждый, я решил добавить индивидуальную настройку часового пояса.

Для выполнения настройки требуется написать команду /tz, о чем предупредит бот пользователя, если он еще не указал свой часовой пояс:



(из-за того что часовой пояс не указан, в ответе используется не локальное время, а гринвичское)

При вводе команды /tz запускается процесс определения часового пояса и появляется клавиатура с тремя кнопками:



  1. Использование локации пользователя.
  2. Ручной ввод.
  3. Отмена.

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

Пример использования


Вторая кнопка позволяет вручную ввести свою часовую зону в формате HH:MM,
где плюс или минус, HH часы, MM минуты.

Пример использования


Третья кнопка отменяет процесс определения.

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

Финальные доработки


Закончив с основным функционалом, я добавил главную клавиатуры с основными функциями, откорректировал ответы для команд /start и /help, ну и по мелочам.

Также я решил заменить часовой пояс по умолчанию для всех пользователей на Московский.

Результат


Главным преимуществом бота SmartScheduler является конечно же парсинг даты, который позволяет даже в самых непредсказуемых ситуациях быстро и удобно записать нужную информацию или задачу в форме напоминания.

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

Заключение


До разработки ботов на node js я был совершенно не знаком с javascriptом, посему все знания, которые я использовал в написании кода, брались из интернета, где зачастую можно найти не совсем то, что тебе на самом деле нужно. Из-за этого, скорее всего, где-то в моем коде встречаются очень глупые ошибки, для определения которых я еще недостаточно много знаю.

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

Но если данная разработка будет пользоваться спросом, то я с удовольствием продолжу улучшать её, а если у вас есть какие-то идеи или вы обнаружили баг, то добро пожаловать на гитхаб проекта. У меня уже есть идеи где и как что изменить (например переписать ужасный спагетти-код в модуле парсинга с использованием нормального Visitor-паттерна), а также в планах добавление поддержки английского языка.

Надеюсь, что этот бот поможет вам также, как помогает мне и моей семье в повседневных делах.

Спасибо за внимание!
Подробнее..

Реализация аудиоконференций в Telegram Asterisk

25.09.2020 20:20:19 | Автор: admin


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

Зачем ?


Многим не нравится что в Telegram нельзя осуществлять групповые звонки.
Ну не использовать же Viber ?)
Также есть ряд кейсов именно для такой реализации, например:
  • Для проведения анонимных аудиоконференций, когда не хочется засветить свой номер либо id среди участников конференции (сразу на ум приходит шабаш хакеров либо клуба анонимных алкоголиков). Не нужно находиться в какой либо группе, сообществе, канале
  • Когда не известно кто подключиться к конференции вообще, но нужно ограничить доступ паролем
  • Все прелести Asterisk: управление конференцией (mute/umute, kick), организация гибридных аудиоконференций с участием клиентов, зарегистрированных на asterisk, telegram и PSTN. Неплохо можно сэкономить на международных звонках
  • Организация корпоративного callback via telegram и т.п.

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

Связка Asterisk VoIP- Telegram VoIP


Сама связка VoIP реализована благодаря библиотеки tg2sip. Использование ее описано в самом репозитории в разделе Usage. Есть еще несколько статей по настройке. Даже есть Docker образ.
Описание этой связки выходит за рамки данной статьи.
Единственный нюанс который я хотел бы озвучить это то, что нельзя позвонить на telegram_id, номера которого нет в Вашей книге контактов. Поэтому звонить нужно на номер телефона, на который зарегистрирован telegram.

В моем боте реализованы как публичные аудиоконференции (Эфиры), к которым может подключиться любой желающий, так и приватные аудиоконференции с доступом по паролю. Приватные комнаты/пароли создают сами пользователи и могут использовать бот в качестве площадки дле проведения аудиоконференций, совещаний и т.п.

Взаимодействие telegram bot Asterisk


Схема взаимодействия в моем боте выглядит следующим образом.
Пользователь выбирает желаемую комнату в меню бота, бот вызывает функцию взаимодействия с Asterisk по API передав в POST запросе параметры для подключения:
номер телефона абонента
идентификатор конференц комнаты
callerid для презентации в конференц комнате
язык для озвучивания пользователю уведомлений в системе Asterisk на родном языке
Далее, Asterisk осуществляет исходящий звонок через каналы telegram на номер, указанный в параметрах запроса. После ответа пользоветелем на звонок, Asterisk подключает его в соответствующую комнату.

Можно было бы использовать прямое подключение с бота на Astersik AMI, но я предпочитаю работать через API, чем проще тем лучше.

API на стороне Asterisk сервера


Код простого API на python. Для инициализации звонка используются .call файлы

#!/usr/bin/python3from flask import Flask, request, jsonifyimport codecsimport jsonimport globimport shutilapi_key = "s0m3_v3ry_str0ng_k3y"app = Flask(__name__)@app.route('/api/conf', methods= ['POST'])def go_conf():    content = request.get_json()    ## Блок авторизации    if not "api_key" in content:        return jsonify({'error': 'Authentication required', 'message': 'Please specify api key'}), 401    if not content["api_key"] == api_key:        return jsonify({'error': 'Authentication failure', 'message': 'Wrong api key'}), 401    ## Проверка наличия нужных параметров в запросе    if not "phone_number" in content or not "room_name" in content or not "caller_id" in content:        return jsonify({'error': 'not all parameters are specified'}), 400    if not "lang" in content:        lang = "ru"    else:        lang = content["lang"]    phone_number = content["phone_number"]    room_name = content["room_name"]    caller_id = content["caller_id"]    calls = glob.glob(f"/var/spool/asterisk/outgoing/*-{phone_number}*")    callfile = "cb_conf-" + phone_number + "-" + room_name + ".call"    filename = "/var/spool/asterisk/" + callfile    if calls:        return jsonify({'message': 'error', "text": "call already in progress"})    with codecs.open(filename, "w", encoding='utf8') as f:        f.write("Channel: LOCAL/" + phone_number + "@telegram-out\n")        f.write("CallerID: <" + caller_id + ">\n")        f.write("MaxRetries: 0\nRetryTime: 5\nWaitTime: 30\n")        f.write("Set: LANG=" + lang + "\nContext: conf-in\n")        f.write("Extension: " + room_name + "\nArchive: Yes\n")    shutil.chown(filename, user="asterisk", group="asterisk")    shutil.move(filename, "/var/spool/asterisk/outgoing/" + callfile)    return jsonify({'message': 'ok'})if __name__ == '__main__':    app.run(debug=True,host='0.0.0.0', port=8080)


При этом диалплан Asterisk в простом виде выглядит следующим образом:

[telegram-out]
exten => _+.!,1,NoOp()
same => n,Dial(SIP/${EXTEN}@telegram)

exten => _X!,1,NoOp()
same => n,Dial(SIP/+${EXTEN}@telegram)

[conf-in]
exten => _.!,1,NoOp()
same => n,Answer()
same => n,Wait(3)
same => n,Playback(beep)
same => n,Set(CHANNEL(language)=${LANG})
same => n,ConfBridge(${EXTEN})
same => n,Hangup


Данное API можно использовать и в других кейсах, например, для организации той же callback кнопки Перезвонить мне и т.п.

Функция вызова API


telephony_api.py
import requests# Заменить example.com на Ваш urlurl = "http://example.com:8080/api/conf"api_key = "s0m3_v3ry_str0ng_k3y"def go_to_conf(phone_number, room_name, caller_id, lang="ru"):    payload = "{\n\t\"phone_number\" : \""+phone_number+"\",\n\t\"room_name\" : \""+room_name+"\",\n    \"caller_id\" : \""+caller_id+"\",\n\t\"lang\": \""+lang+"\",\n\t\"api_key\": \""+api_key+"\"\n}"    headers = {        'content-type': "application/json",        'cache-control': "no-cache",        }    try:        response = requests.request("POST", url, data=payload, headers=headers, timeout=2, verify=False)        if "call already in progress" in response.text:            return False, "Ошибка. Звонок еще не завершен."        elif "error" in response.text:            print(response.text)            return False, "Ошибка. Произошел сбой. Попробуйте позже."        else:            return True, response.text    except:        return False, "Ошибка. Произошел сбой. Попробуйте позже."


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

Пример бота для инициализации вызова в конференц комнату



#!/usr/bin/python3.6import telebotfrom telephony_api import go_to_confbot = telebot.TeleBot("ВашTOKEN")pnone_number = "799999999999"# Ваш номер телефона, на который зарегистрирован telegram аккаунт@bot.message_handler(content_types=['text'])def main_text_handler(message):    if message.text == "Подключи меня в аудиоконференцию":        bot.send_message(message.chat.id, "Ok. Сейчас на telegram Вам прийдет звонок, ответив на который Вы будете подключены в аудиоконференцию")        func_result, func_message = go_to_conf(pnone_number, "ROOM1", "Bob", "ru")        if not func_result:            bot.send_message(chat_id=message.chat.id, text=func_message)if __name__ == "__main__":)   print("bot started")   bot.polling(none_stop=True)

В данном примере номер телефона задан статически, в реальности же можно например, делать запросы в базу на соответствие message.chat.id номер телефона.

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

Практическое применение аннотации в Java на примере создания Telegram-бота

17.12.2020 14:07:58 | Автор: admin
Рефлексия в Java это специальное API из стандартной библиотеки, которая позволяет получить доступ к информации о программе во время выполнения.

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

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

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




Рефлексия



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

Reflection vs Introspection


Наряду с рефлексией также есть понятие интроспекции. Интроспекция это способность программы получить данные о типе и других свойствах объекта. Например, это instanceof:

if (obj instanceof Cat) {   Cat cat = (Cat) obj;   cat.meow();}

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

Некоторые возможности рефлексии


Если говорить конкретнее, то рефлексия это возможность программы исследовать себя во время выполнения и с помощью неё изменять своё поведение.

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

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

Object obj = new Cat();    // а куда кошка пропала?

Воспользуемся рефлексией и создадим экземпляр класса:

Object obj = Class.forName("complete.classpath.MyCat").newInstance();

Давайте также через рефлексию вызовем его метод:

Method m = obj.getClass().getDeclaredMethod("meow");m.invoke(obj);

От теории к практике:

import java.lang.reflect.Method;import java.lang.Class;public class Cat {    public void meow() {        System.out.println("Meow");    }        public static void main(String[] args) throws Exception {        Object obj = Class.forName("Cat").newInstance();         Method m = obj.getClass().getDeclaredMethod("meow");         m.invoke(obj);    }}

Поиграть с ним можно в Jdoodle.
Несмотря на простоту, в этом коде происходит довольно много сложных вещей, и зачастую программисту не хватает лишь просто использования getDeclaredMethod and then invoke.

Вопрос #1
Почему в invoke методе в примере сверху мы должны передавать экземпляр объекта?

Далее углубляться я не буду, так как мы уйдём далеко от темы. Вместо этого я оставлю ссылку на статью старшего коллеги Тагира Валеева.

Аннотации


Важной частью языка Java являются аннотации. Это некоторый дескриптор, который можно повесить на класс, поле или метод. Например, вы могли видеть аннотацию @Override:

public abstract class Animal {    abstract void doSomething();}public class Cat extends Animal {    @Override    public void doSomething() {        System.out.println("Meow");    }}

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

Типы аннотаций


Рассмотрим вышеприведённую аннотацию:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {}

@Target указывает к чему применима аннотация. В данном случае, к методу.

@Retention длительность жизни аннотации в коде (не в секундах, разумеется).

@interface является синтаксисом для создания аннотаций.

Если с первым и последним все более менее понятно (подробнее см.@Targetвдокументации), то@Retentionдавайте разберем сейчас, так как он поможет разделить аннотации на несколько типов, что очень важно понимать.

Эта аннотация может принимать три значения:


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

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

В третьем случае аннотация будет удалена компилятором (её не будет в байт-коде). Обычно это бывают аннотации, которые полезны только для компилятора.

Возвращаясь к аннотации@Override, мы видим, что она имеетRetentionPolicy.SOURCEчто в общем-то логично, учитывая, что он используется только компилятором. В рантайме эта аннотация действительно ничего полезного не дает.

SuperCat


Попробуем добавить свою аннотацию (это здорово нам пригодится во время разработки).

abstract class Cat {    abstract void meow();}public class Home {    private class Tom extends Cat {        @Override        void meow() {            System.out.println("Tom-style meow!"); // <---        }    }        private class Alex extends Cat {        @Override        void meow() {            System.out.println("Alex-style meow!"); // <---        }    }}

Пусть у нас в доме будет два котика: Том и Алекс. Создадим аннотацию для суперкотика:

@Target(ElementType.TYPE)     // чтобы использовать для класса@Retention(RetentionPolicy.RUNTIME)  // хотим чтобы наша аннотация дожила до рантайма@interface SuperCat {}// ...    @SuperCat   // <---    private class Alex extends Cat {        @Override        void meow() {            System.out.println("Alex-style meow!");        }    }// ...

При этом Тома мы оставим обычным котом (мир несправедлив). Теперь попробуем получить классы, которые были аннотированы данным элементом. Было бы неплохо иметь такой метод у самого класса аннотации:

Set<class<?>> classes = SuperCat.class.getAnnotatedClasses();

Но, к сожалению, такого пока метода нет. Тогда как нам найти эти классы?

ClassPath


Это параметр, который указывает на пользовательские классы.

Надеюсь, вы с ними знакомы, а если нет, то спешите изучить это, так как это одна из фундаментальных вещей.

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

public static void main(String[] args) throws ClassNotFoundException {    String packageName = "com.apploidxxx.examples";    ClassLoader classLoader = Home.class.getClassLoader();        String packagePath = packageName.replace('.', '/');    URL urls = classLoader.getResource(packagePath);        File folder = new File(urls.getPath());    File[] classes = folder.listFiles();        for (File aClass : classes) {        int index = aClass.getName().indexOf(".");        String className = aClass.getName().substring(0, index);        String classNamePath = packageName + "." + className;        Class<?> repoClass = Class.forName(classNamePath);            Annotation[] annotations = repoClass.getAnnotations();        for (Annotation annotation : annotations) {            if (annotation.annotationType() == SuperCat.class) {                System.out.println(                  "Detected SuperCat!!! It is " + repoClass.getName()                );            }        }        }}

Не советую использовать это в вашей программе. Код приведён только для ознакомительных целей!

Этот пример показателен, но используется только для учебных целей из-за этого:

Class<?> repoClass = Class.forName(classNamePath);

Дальше мы узнаем, почему. А пока разберём по строчкам весь код сверху:

// ...// пакет в котором мы сейчас находимсяString packageName = "com.apploidxxx.examples";// Загрузчик классов, чтобы получить наши классы из байт-кодаClassLoader classLoader = Home.class.getClassLoader();// com.apploidxxx.examples -> com/apploidxxx/examplesString packagePath = packageName.replace('.', '/');URL urls = classLoader.getResource(packagePath);File folder = new File(urls.getPath());// Наши классы в виде файловFile[] classes = folder.listFiles();// ...

Чтобы разобраться, откуда мы берём эти файлы, рассмотрим JAR-архив, который создаётся, когда мы запускаем приложение:

com   apploidxxx       examples               Cat.class               Home$Alex.class               Home$Tom.class               Home.class               Main.class               SuperCat.class

Таким образом, classes это только наши скомпилированные файлы в виде байт-кода. Тем не менее File это ещё не загруженный файл, мы только знаем, где они находятся, но мы пока по-прежнему не можем видеть, что находится внутри них.

Поэтому загрузим каждый файл:

for (File aClass : classes) {    // имя файла, на самом деле, Home.class, Home$Alex.class и тд    // поэтому нам нужно избавиться от .class и получить путь к файлу    // как к объекту внутри Java    int index = aClass.getName().indexOf(".");    String className = aClass.getName().substring(0, index);    String classNamePath = packageName + "." + className;    // classNamePath = com.apploidxxx.examples.Home    Class<?> repoClass = Class.forName(classNamePath);}

Всё, что сделано ранее, было только для того, чтобы вызвать этот метод Class.forName, который загрузит необходимый нам класс. Итак, финальная часть это получение всех аннотаций, использованных на класс repoClass, а затем проверка, являются ли они аннотацией @SuperCat:

Annotation[] annotations = repoClass.getAnnotations();for (Annotation annotation : annotations) {    if (annotation.annotationType() == SuperCat.class) {        System.out.println(          "Detected SuperCat!!! It is " + repoClass.getName()        );    }}output: Detected SuperCat!!! It is com.apploidxxx.examples.Home$Alex

И готово! Теперь, когда у нас есть сам класс, то мы получаем доступ ко всем методам рефлексии.

Рефлексируем


Как и в примере сверху, мы можем просто создать новый экземпляр нашего класса. Но перед этим разберём несколько формальностей.

  • Во-первых, кошкам надо где-то жить, поэтому им нужен дом. В нашем случае они не могут существовать без дома.
  • Во-вторых, создадим список суперкотов.

List<cat> superCats = new ArrayList<>();final Home home = new Home();    // дом, где будут жить наши котики

Итак, обработка обретает финальную форму:

for (Annotation annotation : annotations) {    if (annotation.annotationType() == SuperCat.class) {        Object obj = repoClass          .getDeclaredConstructor(Home.class)          .newInstance(home);        superCats.add((Cat) obj);    }}

И снова рубрика вопросов:

Вопрос #2
Что будет, если мы пометим @SuperCat класс, который не наследуется от Cat?

Вопрос #3
Почему нам нужен конструктор, который принимает тип аргумента Home?

Подумайте пару минут, а затем сразу разберём ответы:

Ответ #2: Будет ClassCastException, так как сама аннотация @SuperCat не гарантирует того, что класс, помеченный этой аннотацией, наследует что-то или имплементирует.

Вы можете проверить это, убрав extends Cat у Alex. Заодно вы убедитесь в том, насколько полезной может быть аннотация @Override.

Ответ #3: Кошкам нужен дом, потому что они являются внутренними классами. Всё в рамках спецификации The Java Language Specification глава 15.9.3.

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

Подведём итоги и получим: Home.java

package com.apploidxxx.examples;import java.io.File;import java.lang.annotation.*;import java.lang.reflect.InvocationTargetException;import java.net.URL;import java.util.ArrayList;import java.util.List;@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@interface SuperCat {}abstract class Cat {    abstract void meow();}public class Home {    public class Tom extends Cat {        @Override        void meow() {            System.out.println("Tom-style meow!");        }    }        @SuperCat    public class Alex extends Cat {        @Override        void meow() {            System.out.println("Alex-style meow!");        }    }        public static void main(String[] args) throws Exception {            String packageName = "com.apploidxxx.examples";        ClassLoader classLoader = Home.class.getClassLoader();            String packagePath = packageName.replace('.', '/');        URL urls = classLoader.getResource(packagePath);            File folder = new File(urls.getPath());        File[] classes = folder.listFiles();            List<Cat> superCats = new ArrayList<>();        final Home home = new Home();            for (File aClass : classes) {            int index = aClass.getName().indexOf(".");            String className = aClass.getName().substring(0, index);            String classNamePath = packageName + "." + className;            Class<?> repoClass = Class.forName(classNamePath);            Annotation[] annotations = repoClass.getAnnotations();            for (Annotation annotation : annotations) {                if (annotation.annotationType() == SuperCat.class) {                    Object obj = repoClass                      .getDeclaredConstructor(Home.class)                      .newInstance(home);                    superCats.add((Cat) obj);                }            }        }            superCats.forEach(Cat::meow);    }}output: Alex-style meow!

Так что не так с Class.forName?

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

Представьте себе, что вы работаете над проектов в котором 1000 и больше классов (всё-таки на Java пишем). И представьте, что вы будете загружать каждый класс, который найдёте в classPath. Сами понимаете, что память и остальные ресурсы JVM не резиновые.

Способы работы с аннотациями


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

Но всё же Spring вроде работает. Неужели из-за них моя программа такая медленная? К сожалению или к счастью, нет. Spring работает исправно (в этом плане), потому что использует несколько иной способ для работы с ними.

Прямо в байт-код


Все (надеюсь) так или иначе имеют представление, что такое байт-код. В нём хранится вся информация о наших классах и их метаданных (в том числе аннотаций).

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

Так почему бы нам её просто не прочитать (да, из байт-кода)? Но здесь я не буду реализовывать программу для её чтения из байт-кода, так как это заслуживает отдельной статьи. Впрочем, вы сами можете это сделать это будет отличной практикой, которая закрепит материал статьи.

Для ознакомления с байт-кодом вы можете начать с моей статьи. Там я описываю базовые вещи байт-кода с программой Hello World! Статья будет полезна, даже если вы не собираетесь напрямую работать с байт-кодом. В нем описываются фундаментальные моменты, которые помогут ответить на вопрос: почему именно так?

После этого добро пожаловать на официальную спецификацию JVM. Если вы не хотите разбирать байт-код вручную (по байтам), то посмотрите в сторону таких библиотек, как ASM и Javassist.

Reflections


Reflections библиотека с WTFPL лицензией, которая позволяет делать с ней всё, что вы захотите. Довольно быстрая библиотека для различной работы с classpath и метаданными. Полезным является то, что она может сохранять информацию о уже некоторых прочитанных данных, что позволяет сэкономить время. Можете покопаться внутри и найти класс Store.

package com.apploidxxx.examples;import org.reflections.Reflections;import java.lang.reflect.InvocationTargetException;import java.util.Optional;import java.util.Set;public class ExampleReflections {    private static final Home HOME = new Home();    public static void main(String[] args) {            Reflections reflections = new Reflections("com.apploidxxx.examples");            Set<Class<?>> superCats = reflections          .getTypesAnnotatedWith(SuperCat.class);            for (Class<?> clazz : superCats) {            toCat(clazz).ifPresent(Cat::meow);        }    }        private static Optional<Cat> toCat(Class<?> clazz) {        try {            return Optional.of((Cat) clazz                               .getDeclaredConstructor(Home.class)                               .newInstance(HOME)                              );        } catch (InstantiationException |                  IllegalAccessException |                  InvocationTargetException |                  NoSuchMethodException e)         {            e.printStackTrace();            return Optional.empty();        }    }}

spring-context


Я бы рекомендовал использовать библиотеку Reflections, так как внутри она работает через javassist, что свидетельствует о том, что используется чтение байт-кода, а не его загрузка.

Тем не менее существует множество других библиотек, работающих схожим образом. Их достаточно много, но сейчас я хочу разобрать лишь один из них это spring-context. Он, может быть, лучше первого, когда вы разрабатываете бота в среде Spring. Но и здесь есть пара нюансов.

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

Другое дело если вы хотите, чтобы ваши помеченные классы были бинами, тогда вы можете сделать это вручную через ClassPathScanningCandidateComponentProvider, который работает через ASM.

Опять же, довольно редко вы должны будете использовать именно такой метод, но как вариант его стоит рассмотреть.
Я писал на нём бота для ВК. Вот репозиторий, с которым вы можете ознакомиться, но писал я его давно, а когда зашёл посмотреть, чтобы вставить ссылку в статью, то увидел, что через VK-Java-SDK я получаю сообщения с неинициализированными полями, хотя раньше всё работало.

Самое забавное, что я даже версию SDK не менял, поэтому, если вы найдёте, в чем была причина, буду благодарен. Тем не менее загрузка самих команд работает нормально, и это именно то, на что вы можете посмотреть, если хотите увидеть пример работы со spring-context.

Команды в нём представляют из себя вот что:

@Command(value = "hello", aliases = {"привет", "йоу"})public class Hello implements Executable {    public BotResponse execute(Message message) throws Exception {        return BotResponseFactoryUtil.createResponse("hello-hello",                                                      message.peerId);    }}

Примеры кода с аннотацией SuperCat вы можете найти в этом репозитории.

Практическое применение аннотаций в создании Телеграм-бота


Всё это было довольно длинным, но необходимым вступлением для работы с аннотациями. Далее, мы будем реализовывать бота, но цель статьи это не мануал к его созданию. Это практическое применение аннотаций. Здесь могли быть что угодно: от консольных приложений до этих же самых ботов для вк, телеги и прочего.

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

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

Мы будем использовать библиотеку TelegramBots с MIT лицензией для работы с API телеграма. Вы же можете использовать любую другую. Я выбрал её, потому что она могла работать как c (имеет версию со стартёром), так и без спринг-бута.

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

Reflections


Первый бот на очереди это бот, написанный на библиотеке reflections, без Spring. Будем разбирать не всё подряд, а лишь основные моменты, в особенности нас интересует обработка аннотаций. До разбора в статье вы сами можете разобраться в его работе в моём репозитории.

Во всех примерах будем придерживаться того, что бот состоит из нескольких команд, причём эти команды мы не будем загружать вручную, а просто будем добавлять аннотации. Вот пример команды:
@Handler("/hello")public class HelloHandler implements RequestHandler {    private static final Logger log = LoggerFactory      .getLogger(HelloHandler.class);        @Override    public SendMessage execute(Message message) {        log.info("Executing message from : " + message.getText());        return SendMessage.builder()                .text("Yaks")                .chatId(String.valueOf(message.getChatId()))                .build();    }}@Retention(RetentionPolicy.RUNTIME)public @interface Handler {    String value();}

В этом случае параметр /hello будет записан в value в аннотации. value это что-то вроде аннотации по умолчанию. То есть @Handler("/hello") = @Handler(value = "/hello").

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

@Retention(RetentionPolicy.RUNTIME)public @interface Log {    String value() default ".*";    // regex    ExecutionTime[] executionTime() default ExecutionTime.BEFORE;}default` означает, что значение будет применено, если не будет указан `value@Logpublic class LogHandler implements RequestLogger {    private static final Logger log = LoggerFactory      .getLogger(LogHandler.class);        @Override    public void execute(Message message) {        log.info("Just log a received message : " + message.getText());    }}

Но также мы можем добавить параметр, чтобы логгер срабатывал при определённых сообщениях:

@Log(value = "/hello")public class HelloLogHandler implements RequestLogger {    public static final Logger log = LoggerFactory      .getLogger(HelloLogHandler.class);    @Override    public void execute(Message message) {        log.info("Received special hello command!");    }}

Или срабатывал после обработки запроса:

@Log(executionTime = ExecutionTime.AFTER)public class AfterLogHandler implements RequestLogger {    private static final Logger log = LoggerFactory      .getLogger(AfterLogHandler.class);        @Override    public void executeAfter(Message message, SendMessage sendMessage) {        log.info("Bot response >> " + sendMessage.getText());    }}

Или и там, и там:

@Log(executionTime = {ExecutionTime.AFTER, ExecutionTime.BEFORE})public class AfterAndBeforeLogger implements RequestLogger {    private static final Logger log = LoggerFactory      .getLogger(AfterAndBeforeLogger.class);    @Override    public void execute(Message message) {        log.info("Before execute");    }        @Override    public void executeAfter(Message message, SendMessage sendMessage) {        log.info("After execute");    }}

Мы можем делать такое, так как executionTime принимает массив значений. Принцип работы прост, поэтому приступим к обработке этих аннотаций:

Set<Class<?>> annotatedCommands =   reflections.getTypesAnnotatedWith(Handler.class);final Map<String, RequestHandler> commandsMap = new HashMap<>();final Class<RequestHandler> requiredInterface = RequestHandler.class;for (Class<?> clazz : annotatedCommands) {    if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {        for (Constructor<?> c : clazz.getDeclaredConstructors()) {            //noinspection unchecked            Constructor<RequestHandler> castedConstructor =               (Constructor<RequestHandler>) c;            commandsMap.put(extractCommandName(clazz),                             OBJECT_CREATOR.instantiateClass(castedConstructor));        }    } else {        log.warn("Command didn't implemented: "                  + requiredInterface.getCanonicalName());        }}// ...private static String extractCommandName(Class<?> clazz) {    Handler handler = clazz.getAnnotation(Handler.class);    if (handler == null) {        throw new           IllegalArgumentException(            "Passed class without Handler annotation"            );    } else {        return handler.value();    }}

По сути, мы просто создаём мапу с именем команды, которую берём из значения value в аннотации. Исходный код здесь.

То же самое делаем с Log, только логгеров с одинаковыми паттернами может быть несколько, поэтому мы чуть меняем нашу структуру данных:

Set<Class<?>> annotatedLoggers = reflections.getTypesAnnotatedWith(Log.class);final Map<String, Set<RequestLogger>> commandsMap = new HashMap<>();final Class<RequestLogger> requiredInterface = RequestLogger.class;for (Class<?> clazz : annotatedLoggers) {    if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {        for (Constructor<?> c : clazz.getDeclaredConstructors()) {            //noinspection unchecked            Constructor<RequestLogger> castedConstructor =               (Constructor<RequestLogger>) c;            String name = extractCommandName(clazz);            commandsMap.computeIfAbsent(name, n -> new HashSet<>());            commandsMap              .get(extractCommandName(clazz))              .add(OBJECT_CREATOR.instantiateClass(castedConstructor));        }        } else {        log.warn("Command didn't implemented: "                  + requiredInterface.getCanonicalName());    }}

На каждый паттерн приходится несколько логгеров. Остальное всё так же.
Теперь в самом боте нам нужно настроить executionTime и перенаправлять запросы на эти классы:

public final class CommandService {    private static final Map<String, RequestHandler> commandsMap       = new HashMap<>();    private static final Map<String, Set<RequestLogger>> loggersMap       = new HashMap<>();        private CommandService() {    }        public static synchronized void init() {        initCommands();        initLoggers();    }        private static void initCommands() {        commandsMap.putAll(CommandLoader.readCommands());    }        private static void initLoggers() {        loggersMap.putAll(LogLoader.loadLoggers());    }        public static RequestHandler serve(String message) {        for (Map.Entry<String, RequestHandler> entry : commandsMap.entrySet()) {            if (entry.getKey().equals(message)) {                return entry.getValue();            }        }            return msg -> SendMessage.builder()                .text("Команда не найдена")                .chatId(String.valueOf(msg.getChatId()))                .build();    }        public static Set<RequestLogger> findLoggers(      String message,       ExecutionTime executionTime    ) {        final Set<RequestLogger> matchedLoggers = new HashSet<>();        for (Map.Entry<String, Set<RequestLogger>> entry:loggersMap.entrySet()) {            for (RequestLogger logger : entry.getValue()) {                    if (containsExecutionTime(                  extractExecutionTimes(logger), executionTime                ))                 {                    if (message.matches(entry.getKey()))                        matchedLoggers.add(logger);                }            }            }            return matchedLoggers;    }        private static ExecutionTime[] extractExecutionTimes(RequestLogger logger) {        return logger.getClass().getAnnotation(Log.class).executionTime();    }        private static boolean containsExecutionTime(      ExecutionTime[] times,      ExecutionTime executionTime    ) {        for (ExecutionTime et : times) {            if (et == executionTime) return true;        }            return false;    }}public class DefaultBot extends TelegramLongPollingBot {    private static final Logger log = LoggerFactory.getLogger(DefaultBot.class);    public DefaultBot() {        CommandService.init();        log.info("Bot initialized!");    }        @Override    public String getBotUsername() {        return System.getenv("BOT_NAME");    }        @Override    public String getBotToken() {        return System.getenv("BOT_TOKEN");    }        @Override    public void onUpdateReceived(Update update) {        try {            Message message = update.getMessage();            if (message != null && message.hasText()) {                // run "before" loggers                CommandService                  .findLoggers(message.getText(), ExecutionTime.BEFORE)                  .forEach(logger -> logger.execute(message));                    // command execution                SendMessage response;                this.execute(response = CommandService                             .serve(message.getText())                             .execute(message));                    // run "after" loggers                CommandService                  .findLoggers(message.getText(), ExecutionTime.AFTER)                  .forEach(logger -> logger.executeAfter(message, response));                }        } catch (Exception e) {            e.printStackTrace();        }    }}

Лучше всего узнать код самому и посмотреть в репозитории, а ещё лучше открыть его через IDE. Этот репозиторий подходит для начала работы и ознакомления, но в качестве бота он недостаточно хорош.

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

Во-вторых, сама библиотека TelegramBots, как мне кажется, не особо ориентирована на такую работу (архитектуру) бота. Если же вы будете разрабатывать бота именно на этой библиотеке, то можете использовать Ability Bot, который указан в wiki самой библиотеки. Но очень хочется увидеть полноценную библиотеку с такой архитектурой. Поэтому можете начать писать свою библиотеку!

Спринговый бот


Это приобретает больше смысла при работе с экосистемой спринга:

  • Работа через аннотации не нарушает общей концепции работы спринг-контейнера.
  • Мы можем не создавать команды сами, а получить их из контейнера, пометив наши команды как бины.
  • Получаем отличный DI от спринга.

Вообще использование спринга в качестве каркаса для бота это тема отдельного разговора. Ведь многие могут подумать, что это слишком тяжело для бота (хотя, скорее всего, они и на Java ботов не пишут).

Но я думаю, что, спринг это хорошая среда не только для энтерпрайза/веб-приложений. Просто он содержит в себе очень много как официальных, так и пользовательских библиотек для своей экосистемы (под спрингом имеется в виду Spring Boot).

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

Реализация


Что ж, приступим к самому боту.

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

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

Здесь же я разберу именно чтение команд, хотя в самом репозитории есть ещё пара интересных моментов, которые вы можете рассмотреть самостоятельно.
Реализация очень похожа на бота через Reflections, поэтому аннотации те же.

ObjectLoader.java

@Servicepublic class ObjectLoader {    private final ApplicationContext applicationContext;    public ObjectLoader(ApplicationContext applicationContext) {        this.applicationContext = applicationContext;    }        public Collection<Object> loadObjectsWithAnnotation(      Class<? extends Annotation> annotation    ) {        return applicationContext.getBeansWithAnnotation(annotation).values();    }}

CommandLoader.java

public Map<String, RequestHandler> readCommands() {    final Map<String, RequestHandler> commandsMap = new HashMap<>();        for (Object obj : objectLoader.loadObjectsWithAnnotation(Handler.class)) {        if (obj instanceof RequestHandler) {            RequestHandler handler = (RequestHandler) obj;            commandsMap.put(extractCommandName(handler.getClass()), handler);        }    }        return commandsMap;}

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

Подведём итоги


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

  • Reflections.
  • Spring-Context (без Spring).
  • ApplicationContext из Spring.

Тем не менее я могу дать вам совет, основываясь на своём опыте:

  1. Подумайте, нужен ли вам Spring. Он даёт мощный IoC контейнер и возможности экосистемы, но за всё приходится платить. Обычно я рассуждаю так: если нужны база данных и быстрый старт, то Spring Boot вам нужен. Если же бот достаточно прост, то можно обойтись и без него.
  2. Если же вам не нужны сложные зависимости, то смело используйте Reflections.

Реализация, например, JPA без Spring Data мне кажется довольно трудоёмкой задачей, хотя вы также можете посмотреть на альтернативы в виде micronaut или quarkus, но о них я только наслышан и не имею достаточного опыта, чтобы что-то советовать относительно этого.

Если вы приверженец более чистого подхода с нуля даже без JPA, то посмотрите на этого бота, который работает через JDBC через ВК и Телеграм.

Там вы увидите много записей вида:

PreparedStatement stmt = connection.prepareStatement("UPDATE alias SET aliases=?::jsonb WHERE vkid=?");stmt.setString(1, aliases.toJSON());stmt.setInt(2, vkid);stmt.execute();

Но код имеет двухгодичную давность, так что не советую оттуда брать все паттерны. И вообще я бы не рекомендовал таким заниматься вовсе (работу через JDBC).

Также лично мне не особо нравится работать напрямую с Hibernate. Я уже имел печальный опыт писать DAO и HibernateSessionFactoryUtil (те, кто писал, поймут, о чём я).

Что касается самой статьи, я пытался писать кратко, но достаточно, чтобы, имея в руках только эту статью, вы могли начать разработку. Всё-таки это не глава в книге, а статья на Хабре. Глубже изучить аннотации и вообще рефлексию вы сможете сами, например, создавая того же бота.

Всем удачи! И не забывайте о промокоде HABR, который дает дополнительную скидку 10% к той, что указана на баннере.

image



Подробнее..

Пишем телеграм бота на node.js

13.07.2020 18:21:24 | Автор: admin
С полным кодом можно ознакомиться по ссылке.

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

Прежде всего в контакт-лист телеграмма нужно добавить @botFather и написать ему команду /newBot. Далее задаем имя нашего бота и, если оно не занято, придумываем идентификатор бота, по которому его можно будет найти.


Вот и все, наш телеграмм бот готов и botfather поделился с нами API Token, благодаря которому мы сможем управлять ботом

Далее создадим новый проект, введем npm init и добавим файл bot.js в котором будет разрабатываться наш бот.

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

const { Telegraf } = require('telegraf')const bot = new Telegraf(process.env.BOT_TOKEN) //сюда помещается токен, который дал botFatherbot.start((ctx) => ctx.reply('Welcome')) //ответ бота на команду /startbot.help((ctx) => ctx.reply('Send me a sticker')) //ответ бота на команду /helpbot.on('sticker', (ctx) => ctx.reply('')) //bot.on это обработчик введенного юзером сообщения, в данном случае он отслеживает стикер, можно использовать обработчик текста или голосового сообщенияbot.hears('hi', (ctx) => ctx.reply('Hey there')) // bot.hears это обработчик конкретного текста, данном случае это - "hi"bot.launch() // запуск бота

Поместим api token в наш пример и запуcтим бота.

node bot

Проверим работу нашего бота:


Теперь разберемся что лежит в ctx

Для этого после объявления константы bot мы можем использовать log:

ctx.message.from.first_name

Перезапустим наш проект, введем команду /start и в консоли мы получим объект, в котором мы сможем посмотреть необходимые данные о юзере:

{ "update_id": 375631294, "message": {   "message_id": 11,   "from": {     "id": 222222,     "is_bot": false,     "first_name": "Женя",     "username": "Evgenii",     "language_code": "ru"   },   "chat": {     "id": 386342082,     "first_name": "Женя",     "username": "Evgenii",     "type": "private"   },   "date": 1593015188,   "text": "/start",   "entities": [     {       "offset": 0,       "length": 6,       "type": "bot_command"     }   ] }}

Нас будет интересовать объект message, из которого мы сможем достать имя юзера

ctx.message.from.first_name

И текст, который он отправил боту:

ctx.message.text

Мы знаем что лежит в ctx и теперь мы можем приступить к подключению стороннего api, с помощью которого мы сможем получать статистику по коронавирусу. Для этого я буду использовать библиотеку, которая называется covid19-api. Установим ее в наш проект и заимпортим в файл bot.js:

const covidApi = require('covid19-api')

Далее мы удалим наш обработчик стикеров и сделаем новый обработчик, который отслеживает текст и отправляет запрос, чтобы получить данные о коронавирусе, используя метод getReportsByCountries, который можно найти в документации covid19-api:

bot.on('text', async ctx => {   const covidData = await covidApi.getReportsByCountries(ctx.message.text) //сюда помещаем сообщение юзера   ctx.reply(covidData) //и выведем полученные данные})

Давайте проверим какие данные мы получим. Для примера напишем нашему боту в телеграме: russia:


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


Чтобы это избежать нам нужно сделать обработчик ошибок и оповещать юзера, что такой страны не существует. Так же произведем небольшой рефакторинг кода и сделаем нашего бота более дружелюбным, изменим вывод сообщения, которое отправляется по команде /start, настроем корректное отображения данных о статистике и создадим в отдельном файле константу, которая будет содержать список всех стран на английском языке и будем выводить ее при использовании команды /help.

В итоге получаем код:

const { Telegraf } = require('telegraf');const covidApi = require('covid19-api');const COUNTRIES_LIST = require('./const')const bot = new Telegraf('1170363720:AAFJ4ALJebB8Luh5kt1DStmYYqV3TparhKc')bot.start( ctx => ctx.reply(`   Привет ${ctx.from.first_name}!   Узнай статистику по Коронавирусу.   Введи страну на английском языке и получи статистику.   Получить весь список стран можно по команде /help."`))bot.help( ctx => ctx.reply(COUNTRIES_LIST)) // список всех стран на английском языке можно взять в документации covid19-apibot.on('text', async (ctx) => {   try {       const userText = ctx.message.text       const covidData = await covidApi.getReportsByCountries(userText)       const countryData = covidData[0][0]       const formatData = `           Страна: ${countryData.country},           Случаи: ${countryData.cases},           Смерти: ${countryData.deaths},           Выздоровело: ${countryData.recovered}`       ctx.reply(formatData)   } catch(e) {       ctx.reply('Такой страны не существует, для получения списка стран используй команду /help')   }})bot.launch()

Который срабатывает как нам нужно:


Поздравляю! Мы завершили настройку нашего телеграм бота, который выводит статистику заболевших коронавирусом.
Подробнее..

Телеграм бот для поддержки своими руками

28.01.2021 22:20:35 | Автор: admin

Представьте, что у вас есть свой канал в Телеге. Допустим, вы высказываете непопулярную политическую точку зрения и, соответственно, ловите хейт в личку со стороны читателей и проходящих мимо.

Или, например, вы продаете что-то через свой канал. Клиентов так много, что один "продажник" (=вы) не справляется. Или поддержка вашего бизнеса отвечает всем в публичном чате, который прикреплен к вашему каналу. Но многие стесняются задать вопросы, так как их могут увидеть, поэтому пишут в личку, что не масштабируется.

Проблем много, а решение одно: сделать Телеграм бот, который будет работать посредником между вашими клиентами и командой поддержки.

Мое мнение: это самый лучшее применение телеграм ботов за всю историю их существования. На втором месте - рассылка закрытой информации через бота только проплатившим пользователям.

Самый популярный конструктор таких ботов - Livegrambot. Он позволяет сделать тоже самое, но при этом бот будет писать вашим пользователям "я сделан через Livegrambot", выпрашивая деньги у вас. Будучи умелым создателем Телеграм ботов, я решил сделать свой аналог, но уже с открытым исходным кодом и легким способом запустить его бесплатно на бесплатные серверы.

Ниже я расскажу, как в 1 клик запустить такого бота и как он технически устроен.

TL;DR: Код выложил сюда: https://github.com/ohld/telegram-support-bot

Юзер стори или как с этим ботом работать.

Действующие лица:

  • Ваши Пользователи (читатели канала, клиенты),

  • Закрытый Чат Поддержки (где сидят те, кто будет отвечать на вопросы Пользователей),

  • Бот (которому Пользователи будут писать свои вопросы).

Вот так это все будет работать:

  1. Вы публикуете ссылку на Бота,

  2. Пользователи пишут в него свои вопросы,

  3. Бот пересылает их сообщения в ваш Чат Поддержки,

  4. В этом чате вы или ваши помощники отвечают на сообщение (через reply),

  5. Бот пересылает ответ обратно пользователю от своего лица, скрывая аккаунт отвечающего.

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

Как это все запустить? Желательно, без навыков.

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

В README.md я добавил волшебную кнопку от Heroku, которая поможет запустить код из репозитория. После нажатия, при наличии аккаунта на Heroku (который можно создать также по 1 кнопке), вы увидите такую картину:

Heroku уже понял, какие данные нужно запросить у вас, у создателя, чтобы все завелось автоматически. Давайте пройдемся по этим параметрам:

- App name: название приложения в системе Heroku. Можно придумать любое.

- Choose a region: где Хероку запустит ваш код. Можно выбрать любое место.

- HEROKU_APP_NAME: впишите сюда тоже самое, что указали выше в App name (это важно для того, чтобы завести тг бота через вебхуки).

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

- TELEGRAM_TOKEN: токен вашего бота, который можно получить у BotFather.

Как узнать TELEGRAMSUPPORTCHAT_ID

Способов много, но самый простой - это добавить вот этого бота в ваш созданный приватный чат. Этот бот возвращает все данные, которые ему присылает Телеграм, в частности событие "меня добавили в чат", откуда вы и сможете извлечь chat_id.

Как реализовать такого бота?

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

Примеры кода я буду писать на языке Python и использовать библиотеку python-telegram-bot. Итогда я буду вставлять ссылки на GitHub (гит), чтобы легко можно было найти этот кусок кода в моем репозитории.

Хендлеры (обработчики событий)

Для нашей задумки необходимы всего 3 хендлера (гит):

from telegram.ext import Updaterfrom telegram.ext import CommandHandler, MessageHandler, Filtersupdater = Updater(TELEGRAM_TOKEN)dp = updater.dispatcher# Для приветственного сообщения и для "к вам подключился {username}"dp.add_handler(CommandHandler('start', start))# Для пересылки из бота в чат поддержкиdp.add_handler(MessageHandler(Filters.chat_type.private, forward_to_chat))# Для пересылки ответа из чата обратно пользователюdp.add_handler(MessageHandler(Filters.chat(TELEGRAM_SUPPORT_CHAT_ID) &amp;amp; Filters.reply, forward_to_user))

С командой /start все понятно. Юзер нажал - прислать приветственное сообщение - прислать в чат поддержки о том, что подключился новый юзер (гит).

def start(update, context):    update.message.reply_text(WELCOME_MESSAGE)    user_info = update.message.from_user.to_dict()    context.bot.send_message(        chat_id=TELEGRAM_SUPPORT_CHAT_ID,        text=f"? Connected {user_info}.",    )

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

def forward_to_chat(update, context):    update.message.forward(chat_id=TELEGRAM_SUPPORT_CHAT_ID)

В случае отправление ответа (reply) на пересланное сообщение, необходимо скопировать содержимое сообщения и отправить его от лица бота. Если аналогично сделать .forward, то будет виден отправитель. А тут как раз недавно в Telegram Bot API добавили возможность удобно копировать содержимое сообщения (гит):

def forward_to_user(update, context):    user_id = update.message.reply_to_message.forward_from.id    context.bot.copy_message(        message_id=update.message.message_id,        chat_id=user_id,        from_chat_id=update.message.chat_id    )

Бесплатный деплой на Heroku

Чтобы захостить это все бесплатно на Heroku, бот должен быть запущен в режиме Webhook, а не Pooling. Разница их в том, что вебхук "слушает новые сообщения от Телеги", а пулинг "периодически запрашивает". Чтобы запрашивать, сервер должен работать постоянно (условно, каждую секунду запрашивать у серверов Телеграмма новые сообщения, которые кто-то написал в бот). Однако, в случае с вебхуками, сервер может просто ждать, когда серверы Телеграмма сами отправят нам новые обновления бота.

Этот факт критически важен, если мы хотим бесплатно пользоваться услугами Heroku (который по факту дает нам свои серверы в аренду). Хероку любит "усыплять" простаивающие машины, которые пробуждаются в момент нового входящего запроса. Именно новые сообщения от серверов Телеграмма и будут пробуждать наш сервер тогда, когда необходимо переслать пользовательское сообщение из лички бота в наш чат поддержки.

Для того, чтобы настроить Webhook, необходимо поднять вебсервер, который будет слушать входящие сообщения по endpoint. Сказать Телеграму: "присылай события бота мне на сервер - по этому адресу". Также нужно как-нибудь защититься от злоумышленников, которые могут отправить на наш вебсервер событие, прикинувшись сервером телеги. Также телеграм требует, чтобы все работало https.

Звучит сложно, однако Heroku автоматически и бесплатно обеспечит https, а вебсервер для вебхука уже встроен в библиотеку python-telegram-bot. Если добавить секретный токен вашего бота в URL, по которому вы будете слушать события от Телеги, то можно защититься от стороннего вмешательства.

Вот как можно запустить Телеграм бот в webhook-режиме (гит) через эту библиотеку:

# запускаем слушающий вебсервер updater.start_webhook(  listen="0.0.0.0",  port=PORT,  # HEROKU требует, чтобы порт вебсервера задавался через переменные окружения  url_path=TELEGRAM_TOKEN  # добавляем секретное значение в адрес, который слушаем)# говорим Телеграму: "присылай события бота по этому адресу"updater.bot.set_webhook(f"https://{HEROKU_APP_NAME}.herokuapp.com/{TELEGRAM_TOKEN}")updater.idle()

Помните, мы отдельно задавали переменную окружения HEROKU_APP_NAME , куда копипастили название нашей Heroku App? Дело в том, что эта переменная используется в адресе, по которому Heroku запускает наш вебсервер. Но при этом, имя приложения Хероку нельзя получить изнутри, поэтому решение "скопипастить название App Name в отдельную переменную окружения" для меня звучит норм.

Что дальше?

Допустим, вы запустили бота, у вас уже много клиентов и вы хотите усовершенствовать функционал телеграм бота. Что можно сделать?

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


Спасибо за просмотр. Теперь вы знаете, как можно сделать и бесплатно задеплоить Телеграм бота поддержки. Полный код проекта (вместе с волшебной кнопкой "задеплой это на хероку") лежит тут. В своем Телеграм канале я делюсь опытом разработки больших телеграм ботов, делюсь датасетами и продуктовой аналитикой. Заходите.

А какие другие популярные юзкейсы Телеграм ботов вы бы выделили? Напишите в комментариях.

Подробнее..

Прыжок до небес запускаем телеграм бота на Python в serverless облаке

16.04.2021 00:22:45 | Автор: admin

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

Очевидно, что для такой задачи, как телеграм бот, этот подход очень удобен.

В этой статье описаны все шаги для запуска бота в Yandex.Cloud Functions. Опоры на код я не делаю. Наша основная задача сейчас - настроить запуск в облаке.

Создадим бота

Чтобы создать телеграмм бота, нужно воспользоваться @BotFather. Для этого используйте команду /new_bot. Скопируйте токен (он будет там, где оранжевая полоса)

Настройка Yandex.Cloud

Для работы с Яндекс.Облаком перейдите на сайт https://cloud.yandex.ru/ и войдите в свой аккаунт. Если вы все сделали правильно, вы увидите рабочий дашборд.

Cloud Functions

  • Перейдите в раздел Cloud Functions

  • Создайте новую функцию c названием, например, python-tg-bot.

  • Укажите язык python и выберите самую последнюю версию (python3.8 на момент написания этой статьи).

  • В настройках функции загрузите код бота. Скачать архив можно по ссылке. Параметры укажите как на картинке. В переменные окружения добавьте поле BOT_TOKEN и вставьте токен, полученный в самом начале. После обязательно нажмите Создать версию, чтобы запустить функцию.

  • Запомним идентификатор функции (первая строка)

API-Gateway

Чтобы мы смогли получить доступ к нашей функции, нужно настроить API-Gateway.

  • Перейдите в раздел API-Gateway

  • Создайте новый шлюз и настройте его.Скопируйте конфигурацию и замените YOUR_FUNCTION_ID на идентификатор функции, полученный ранее.

openapi: 3.0.0info:title: for-python-tg-botversion: 1.0.0paths:/:post:x-yc-apigateway-integration:type: cloud-functionsfunction_id: YOUR_FUNCTION_IDoperationId: tg-webhook-function
  • Запомним ссылку, по которой можно вызвать нашу функцию

Устанавливаем webhook

Теперь нужно сообщить телеграму, куда отправлять информацию о новых сообщениях.

Для этого:

  • Установите библиотеку pip install pyTelegramBotAPI

  • Запустите питоновский скрипт:

import telebotbot = telebot.TeleBot("YOUR_TOKEN")bot.remove_webhook()bot.set_webhook("YOUR_URL")

Тестируем

Сделали "эхо-бота". Что дальше?

Как говорилось в начале, такой способ запустить бота очень легок для разработчика, но что же делать, если нам нужна база данных или сложные api-запросы к другим ресурсам. Все это можно реализовать в Yandex.Cloud. Например, с помощью сервисов Yandex Database (тоже serverless) или Object Storage. Отдельные сервисы можно запустить, как отдельные функции. В следующей статье, я расскажу о том, как подключить базу данных Yandex Database к боту.

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

Тарифы Yandex.Cloud

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

  • Yandex API Gateway

    Каждый месяц не тарифицируются первые 100 000 запросов к API-шлюзам.

  • Yandex Cloud Functions

    Каждый месяц не тарифицируются:

    • первые 1 000 000 вызовов функций;

      \nu = \frac{10^6}{30 * 24 * 60} \approx 23 \text{ } \frac{\text{запроса}}{\text{час}}

    • первые 10 ГБчас выполнения функций.

  • Бессерверный режим Yandex Database

    Каждый месяц не тарифицируются:

    • первые 1 000 000 операций (в единицах RU);

    • первый 1 ГБ/месяц хранения данных.

Полезные ссылки

  1. Репозиторий с кодом бота

  2. Yandex.Cloud

  3. Описание тарифа free tier

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru