Что делает: по нажатию на кнопку в чате с ботом берёт случайную картинку с http.cat и привязав к ней рандомную подпись из массива подписей, заданного в проекте, отсылает в переписку сообщение, давая узнать какой ты сегодня специалист техподдержки.
- Vapor - веб-фреймворк на Swift
- Telegram Vapor Bot - обёртка для облегчения работы с апи телеграма - https://github.com/nerzh/telegram-vapor-bot
- http.cat - сайт, возвращаю смешные картинки с котиками для каждого из http-статусов
- Google Compute Engine - хостинг, на котором можно захостить своего бота для его непрерывной работы
Всё уже было подробно описано вот в этой статье, за что большое спасибо dimabiserov
Для того, чтобы сгенерировать проект на Vapor нужно сначала этот Vapor установить.
- Открыть терминал в нужной папке и если Vapor ещё не установлен выполнить комманды:
brew install vapor
vapor new newtgbot
newtgbot - это название, которое получит проект
- Vapor сгенерит проект, в нём открыть файл Package.swift
- В configure.swift добавить:
app.http.server.configuration.hostname = "0.0.0.0"
app.http.server.configuration.port = 80
- В Package.swift добавить в dependencies:
.package(url: "https://github.com/nerzh/telegram-vapor-bot", .upToNextMajor(from: "2.1.0")),
- В Package.swift добавить targets/dependencies:
.product(name: "TelegramVaporBot", package: "telegram-vapor-bot"),
В итоге у меня вышел такой Package (тесты я удалил потому что не использовал, их можно оставить)
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "SupportOfTheDayBot",
platforms: [
.macOS(.v12)
],
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.83.1"),
.package(url: "https://github.com/nerzh/telegram-vapor-bot", .upToNextMajor(from: "2.1.0")),
],
targets: [
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "TelegramVaporBot", package: "telegram-vapor-bot"),
]
)
]
)
- Создать файл DefaultBotHandlers.swift:
import Vapor
import TelegramVaporBot
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
final class DefaultBotHandlers {
private static let startCommand = "/start"
private static let supCommand = "Какой я сегодня саппорт?"
private static var sups = [
"Загадочный саппорт",
"Разъярённый саппорт",
"Сонный саппорт",
"Гениальный саппорт",
"Весёлый саппорт",
"Опасный саппорт",
"Игривый саппорт",
"Задумчивый саппорт",
"Романтичный саппорт",
"Грустный саппорт",
"Преисполнившийся саппорт",
"Уставший саппорт",
"Больше не саппорт",
"Заряженный саппорт",
"Пьяный саппорт",
"Обматерённый саппорт",
"Саппорт ставший чековым принтером",
"Саппорт синтегрированный с кассой",
"Саппорт забывший где взять данные для привязки ",
"Саппорт хлебнувший тестов акций",
"Расплавленный саппорт",
"Саппорт съевший клиента",
"Саппорт Шрёдингера"
]
private static var codes = [
"100",
"101",
"102",
"103",
"200",
"201",
"202",
"203",
"204",
"205",
"206",
"207",
"300",
"301",
"302",
"303",
"304",
"305",
"306",
"307",
"400",
"401",
"402",
"403",
"404",
"405",
"406",
"407",
"408",
"409",
"410",
"411",
"412",
"413",
"414",
"415",
"416",
"417",
"418",
"422",
"423",
"424",
"425",
"426",
"449",
"450",
"500",
"501",
"502",
"503",
"504",
"505",
"506",
"507",
"509",
"510"
]
static func addHandlers(app: Vapor.Application, connection: TGConnectionPrtcl) async {
await defaultBaseHandler(app: app, connection: connection)
await commandStartHandler(app: app, connection: connection)
}
private static func commandStartHandler(app: Vapor.Application, connection: TGConnectionPrtcl) async {
await connection.dispatcher.add(TGCommandHandler(commands: [startCommand]) { update, bot in
let button: TGKeyboardButton = .init(text: "Какой я сегодня саппорт?")
let rkm: TGReplyKeyboardMarkup = .init(keyboard: [[button]], resizeKeyboard: true)
let reply: TGReplyMarkup = .replyKeyboardMarkup(rkm)
try await update.message?.reply(text: "Тут ты можешь узнать, какой ты сегодня саппорт. Жми на кнопку", bot: bot, replyMarkup: reply)
})
}
/// Handler for all updates
private static func defaultBaseHandler(app: Vapor.Application, connection: TGConnectionPrtcl) async {
await connection.dispatcher.add(TGBaseHandler({ update, bot in
guard let message = update.message else { return }
if message.text == supCommand {
guard let message = update.message else { return }
var img = Data()
if let url = URL(string: "https://http.cat/\(codes[Int.random(in: 0..<codes.count)])") {
img = try Data(contentsOf: url)
}
let photoTG = TGInputFile(filename: "file", data: img)
var params: TGSendPhotoParams = .init(chatId: .chat(message.chat.id), photo: .file(photoTG))
params.caption = sups[Int.random(in: 0..<sups.count)]
try await connection.bot.sendPhoto(params: params)
} else {
// здесь можно добавить рекцию на любое другое сообщение
}
}))
}
}
- Создать файл TGBotConnectionActor:
import Foundation
import TelegramVaporBot
actor TGBotConnection {
private var _connection: TGConnectionPrtcl!
var connection: TGConnectionPrtcl {
self._connection
}
func setConnection(_ conn: TGConnectionPrtcl) {
self._connection = conn
}
}
- Снова в configure.swift добавить в функцию строки, где tgApi - токен бота:
let TGBOT: TGBotConnection = .init()
let tgApi: String = "YYYYYYYYYY:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
/// set level of debug if you needed
TGBot.log.logLevel = app.logger.logLevel
let bot: TGBot = .init(app: app, botId: tgApi)
await TGBOT.setConnection(try await TGLongPollingConnection(bot: bot))
await DefaultBotHandlers.addHandlers(app: app, connection: TGBOT.connection)
try await TGBOT.connection.start()
- Запустить проект у себя. Уже на этом этапе бот будет работать, пока запущен проект.
Далее чуть-чуть расскажу как запустить его на виртуальной машине в Google Compute Engine, чтобы бот мог работать постоянно. Почему я выбрал его? Потому что он даёт какое-то время бесплатно поработать (2-3 месяца) прежде чем платить денюжку.
- Пройти регистрацию в Google Compute Engine
- Зайти в консоль управления console.cloud.google.com
- В левой панели консоли выбрать пункт Compute Engine и в нём VM instances
- В верхней панели кнопок нажать Create Instance
- Тут нужно задать имя будущей виртуальной машины и её характеристики, для бота я выбрал самую дешёвую и самую хилую, с 1 ядром, т.к. логики у данного бота кот наплакал
- На вкладке Firewall можно сразу поставить обе галочки Allow HTTP traffic и Allow HTTPs traffic (без этого будут проблемы с получением запросов от юзеров боту)
- Создание может занять какое-то время, нужно подождать
- Как только ВМ будет создана, она появиться в списке instances. Выделить её галочкой и запустить кнопкой START на панели вверху страницы
- Теперь у этой ВМ в колонке Connect нажать на кнопку SSH, что позволит к ней подключиться
- В открывшейся консоли нужно
sudo apt-get update
потом
sudo apt-get install clang libicu-dev libatomic1 build-essential pkg-config
потом
sudo apt-get install libssl-dev
- Теперь необходимо установить сюда swift. Тк у меня вм создалась с debian на борту, использовать эту инструкцию (https://swift-arm.com/installSwift/)
curl -s https://packagecloud.io/install/repositories/swift-arm/release/script.deb.sh | sudo bash
затем
sudo apt install swiftlang
- Установить Vapor Toolbox (https://docs.vapor.codes/install/linux/)
git clone https://github.com/vapor/toolbox.git
cd toolbox
git checkout master
make install
- Перейти в нужную дерикторию(а можно прямо и здесь) и клонировать проект с гитхаба (мой открытый, поэтому ничего попросит)
git clone https://github.com/smalldevcloud/SupportOfTheDayBot.git
- Перейти в папку с проектом
cd SupportOfTheDayBot
- Из папки с проектом сбилдить и запустить проект
sudo swift build
sudo swift run &
Амперсанд "&" после run необходим для того, чтобы проект продолжил свою работу после того как вы закроете консоль или соедение по ssh прервётся, т.к. это терминал, а в терминале процессы ведут себя так - прекращают свою работу если окно закрывается, если запущено без амперсанда. ---- Если начинает ругаться на какие-нибудь файлы вроде Package.resolved или с расширением .lock, то удалить их (добавить sudo, если access denied) и повторить запуск
- Ещё кроха инфы - как остановить работу сервера, если он был запущен с амперсандом. Первой командой узнаём PID процесса, который запустился на этом порту. Второй останавливаем процесс зная номер PID (в моём примере это 18806)
sudo ss -lptn 'sport = :80'
sudo kill -9 18806