Статья написана с целью передачи проекта всем кто ищет информацию по написанию простых ботов, началу работы с docker и github Actions.
1. Введение
У нас Tg бот написан был года 2,5 назад, на тот момент нужен был помощник для приема заявок от пользователей. Было очень не удобно принимать огромное количество звонков с разных объектов и порой терялись задачи в мессенджерах.
Не так давно, вечерком, начал перерабатывать функционал. Так была добавлена база для хранения заявок, админ панель, и автоматизирована постановка на охрану пары объектов. Обо всем по порядку...
Ссылка на GitHub https://github.com/LeoAlecksey/scripts/tree/main/tg_bot
- Небольшой обзор кода Tg-бота, написан на Python библиотека pyTelegramBotAPI, в нем:
- Используем Telebot-api, MTS Exsolve Api, Mysql (простейшие запросы).
- Напишем Dockerfile для упаковки проекта.
-
Bonus — Используем GitHub Actions для автоматизации сборки
Код рабочий, в данный момент развернут и не вызывает проблем при работе.
Структура проекта
main.py — код основной программы.
button.py — описаны кнопки, которые будут видны в боте.
.env — содержит переменные
Dockerfile — описана сборка контейнера
/.github/workflows/Prod_build_TG_bot.yml — задача по сборке контейнера в GitHub Actions при внесении изменений в main.
requests.db — файл базы данных MySQL, используется для хранения обращений пользователей.
start.sh — позволяет портировать переменные окружения из .env в контейнер и использовать при запуске.
Описание
Представленный бот предоставляет следующие функции:
Тех. поддержка
- Возможность отправки заявок в IT отдел, ограничение — подача заявки одним сообщением.
- Отслеживание статуса обращения пользователем.
- Изменение статуса обращения сотрудниками IT отдела, в т.ч. удаление обращений.
- Админ панель для IT отдела доступна исключительно из группы IT_ADM
Взаимодействие с сигнализацией ИПРО-2
- Возможность производить постановку и снятие с охраны офисов. Доступно только из группы secure.
- Данные о пользователе взаимодействовавшем с сигнализацией отправляются в группу администраторов офиса office
Информация
- Предоставление ссылок на информационные ресурсы компании.
2. Обзор кода main.py
2.1 Импортируем библиотеки и переменные окружения заданные в файле .env
import os
import telebot
from dotenv import load_dotenv
from telebot import *
import button
import requests
import sqlite3
load_dotenv()
token = os.getenv('TOKEN') # Токен TG бота, выдает BotFather.
bot = telebot.TeleBot(token)
secure_chat_id = os.getenv('SECURE') # ID чата группы в телеге, из которого можно взаимодействовать с функционалом сигнализации.
office = os.getenv('OFFICE') # ID чата группы в телеге, в который будут сыпаться оповещения о постановке и снятии охраны определенным пользователем.
it_adm = os.getenv('IT_ADM') # ID чата группы в телеге, в которую будут падать заявки на проведения работ.
api_mts_key = os.getenv('API_MTS_KEY') # Token Api МТС из личного кабинета Exolve.
number_mts = os.getenv('NUMBER') # Номер приобретенный в ЛК МТС Exolve.
number_1 = os.getenv('NUMBER_1') # Номер тел. Сим установленная на объекте 1.
number_2 = os.getenv('NUMBER_2') # Номер тел. Сим установленная на объекте 2.
2.2 Добавляем обработчики:
menu = ['? Тех. поддержка',['admin'],'? Информация',['start'],'?','? Офис 2 ?','? Офис 1 ⚙️','? Офис 2 ?','? Офис 1 ⚙️','⚠️ Проверить статус заявки','❌CLOSE❌','? Оставить заявку','? назад','Постановка на охрану ?♀️','Запрос на снятие с охраны ⚡️']
@bot.message_handler(commands=['admin']) # admin панель (будет доступна только из группы администраторов IT_ADM)
def admin(message):
if str(message.chat.id) == it_adm: # Спасибо за исправления @omgiafs @suprunchuk была найдена уязвимость.
bot.send_message(chat_id=it_adm, text=f'Добрый день!\nВы в меню администратора.\nЧто Вам необходимо?\n', reply_markup=button.markup_admin())
else:
bot.send_message(message.chat.id,'Вы не являетесь администратором', reply_markup=button.markup_start())
@bot.message_handler(commands=['start']) # кнопка Start
def start(message):
bot.send_message(message.chat.id, f"? Привет,{message.from_user.first_name}!", reply_markup=button.markup_start())
bot.send_message(message.chat.id, '❓ Выберете интересующий Вас раздел', reply_markup=button.markup_main())
@bot.message_handler(commands=['close']) # кнопка close
def close(message):
bot.send_message(message.chat.id, 'Goodbye', reply_markup=None)
Функция def admin(message)
выполняет проверку, если админ панель пытаются открыть не из группы администраторов, будет отправлено сообщение пользователю 'Вы не являетесь администратором'.
Добавлены основные обработчики команд, ниже представлены команды которые можно использовать для взаимодействия с ботом:
/start — запускает бота;
/admin — переход в админ панель;
/close — выход из меню бота.
2.3 Основные меню и действия:
@bot.message_handler(func=lambda message: True, content_types=['audio', 'photo', 'voice', 'video', 'document', 'text', 'location', 'contact', 'sticker'])
def get_text_messages(message):
########################## START ##############################
if message.text == '?':
bot.send_message(message.chat.id, ' Вы вернулись в главное меню.\n❓ Выберете интересующий Вас раздел', parse_mode='HTML', reply_markup=button.markup_main())
elif message.text == '? назад':
bot.send_message(message.chat.id, '?', reply_markup=button.markup_main())
########################## MAIN ##############################
elif message.text == '?':
if str(message.chat.id) == secure_chat_id: # Спасибо за исправления @omgiafs @suprunchuk была найдена уязвимость.
bot.send_message(chat_id=secure_chat_id, text=f'Добрый день!\nВы в меню охраны.\nЧто необходимо сделать?\n', reply_markup=button.markup_secure())
else:
bot.send_message(message.chat.id,'Вы не можете использовать данную фунуцию...\nОбратитесь к администратору.', reply_markup=button.markup_start())
elif message.text == '❌CLOSE❌':
bot.send_message(message.chat.id, 'Goodbye', reply_markup=button.markup_start())
elif message.text == '? Тех. поддержка':
bot.send_message(message.chat.id, 'Выберите подменю', reply_markup=button.markup_it())
elif message.text == '? Информация':
bot.send_message(message.chat.id, 'Подробно про компанию по ' + '[ссылке](https://digniori.ru/)', parse_mode='Markdown')
bot.send_message(message.chat.id, 'Облачное хранилище по ' + '[ссылке](https://cloud.digniori-arts.ru/)', parse_mode='Markdown')
########################## SECURE ##############################
elif message.text == 'Постановка на охрану ?♀️':
bot.send_message(chat_id=secure_chat_id, text=f'Какой объект, необходимо поставить на охрану???', reply_markup=button.markup_up())
elif message.text == 'Запрос на снятие с охраны ⚡️':
bot.send_message(chat_id=secure_chat_id, text=f'Какой объект, необходимо снять с охраны???', reply_markup=button.markup_down())
2.4 Действия при выборе постановки или снятия с охраны:
В данной части будем использовать api MTS Exolve c целью автоматизации постановки и снятия с охраны объектов в которых установлена охранная сигнализация ИПРО-2.
Данная задача появилась в связи с тем, что количество поддерживаемых пользователей приложением, для дистанционной постановки и снятия с охраны, ограниченно 5 номерами. В данном случае возможно добавить необходимых пользователей в группу Secure.
elif message.text == '? Офис 1 ⚙️':
try:
headers = {
'Authorization': f'{api_mts_key}',
'Content-Type': 'application/json',
}
data = {"number": f"{number_mts}", "destination": f"{number_ms}", "text" : "O1"}
requests.post('https://api.exolve.ru/messaging/v1/SendSMS', headers=headers, json=data)
bot.send_message(chat_id=secure_chat_id, text=f'Спасибо за информацию, охранная система включена.\nУ Вас есть 15 сек покинуть помещение.', reply_markup=button.markup_start())
bot.send_message(chat_id=secure_chat_id, text=f'?♀️', reply_markup=button.markup_main())
bot.send_message(chat_id=office, text=f'Cотрудник {message.from_user.first_name}, ушел. \nВключена сигнализация на Малой Семеновской ⚙️.\nNikname:@{message.from_user.username}', parse_mode='Markdown')
except Exception as e:
bot.send_message(message.chat.id, 'Произошла ошибка... Попробуйте снова.', reply_markup=button.markup_start())
elif message.text == '? Офис 1 ⚙️':
#try:
headers = {
'Authorization': f'{api_mts_key}',
'Content-Type': 'application/json',
}
data = {"number": f"{number_mts}", "destination": f"{number_ms}", "text" : "O0"}
requests.post('https://api.exolve.ru/messaging/v1/SendSMS', headers=headers, json=data)
bot.send_message(chat_id=secure_chat_id, text=f'Спасибо за информацию, охранная система будет выключена.\nВремя ожидания 20 сек.', reply_markup=button.markup_main())
bot.send_message(chat_id=secure_chat_id, text=f'? Сигнализация отключена', reply_markup=button.markup_main())
bot.send_message(chat_id=office, text=f'Cотрудник {message.from_user.first_name}, отключил сигнализацию на Малой Семеновской ⚙️.\nNikname:@{message.from_user.username}', parse_mode='Markdown')
elif message.text == '? Офис 2 ?':
try:
headers = {
'Authorization': f'{api_mts_key}',
'Content-Type': 'application/json',
}
data = {"number": f"{number_mts}", "destination": f"{number_nvk}", "text" : "O1"}
requests.post('https://api.exolve.ru/messaging/v1/SendSMS', headers=headers, json=data)
bot.send_message(chat_id=office, text=f'''Cотрудник {message.from_user.first_name}, ушел. \nНаправлен запрос на включение сигнализации на Новокузнецкой ?.\nNikname:@{message.from_user.username}''', parse_mode='Markdown')
bot.send_message(chat_id=secure_chat_id, text=f'Спасибо за информацию, охранная система включена.\nУ Вас есть 15 сек покинуть помещение.')
bot.send_message(chat_id=secure_chat_id, text=f'?♀️', reply_markup=button.markup_main())
except Exception as e:
bot.send_message(message.chat.id, 'Произошла ошибка... Попробуйте снова.', reply_markup=button.markup_start())
elif message.text == '? Офис 2 ?':
try:
headers = {
'Authorization': f'{api_mts_key}',
'Content-Type': 'application/json',
}
data = {"number": f"{number_mts}", "destination": f"{number_nvk}", "text" : "O0"}
requests.post('https://api.exolve.ru/messaging/v1/SendSMS', headers=headers, json=data)
bot.send_message(chat_id=secure_chat_id, text=f'Спасибо за информацию, охранная система будет выключена.\nВремя ожидания 20 сек.')
bot.send_message(chat_id=secure_chat_id, text=f'? Сигнализация отключена', reply_markup=button.markup_main())
bot.send_message(chat_id=office, text=f'Cотрудник {message.from_user.first_name}, отключил сигнализацию на Новокузнецкой ?.\nNikname:@{message.from_user.username}', parse_mode='Markdown')
except Exception as e:
bot.send_message(message.chat.id, 'Произошла ошибка... Попробуйте снова.', reply_markup=button.markup_start())
2.5 Обращения пользователей и админ меню:
Административное меню открывается только из группы IT_ADM по команде /admin.
########################## USER_TASKS ##############################
elif message.text == '⚠️ Проверить статус заявки':
bot.send_message(message.chat.id, 'Введите номер Вашего обращения:', reply_markup=button.markup_it())
bot.register_next_step_handler(message, stat_user_tasks)
elif message.text == '? Оставить заявку':
bot.send_message(message.chat.id, f'{message.from_user.first_name}, Введите название объекта и опишите проблему:')
bot.register_next_step_handler(message, add_task)
########################## ADMIN_!!! ##############################
elif message.text == 'Списки задач?':
bot.send_message(chat_id=it_adm, text=f'Какой список Вас интересует?', reply_markup=button.markup_task_l())
elif message.text == '? Изменить статус':
bot.send_message(chat_id=it_adm, text=f'Введите номер задачи для изменения статуса', reply_markup=button.markup_admin())
bot.register_next_step_handler(message, stat_task)
elif message.text == '⛔️Удалить задачу⛔️':
bot.send_message(chat_id=it_adm, text=f'ВНИМАНИЕ\nУдаленные из базы задачи не восстановить!\nЕсли вы уверены, введите номер задачи.', reply_markup=button.markup_admin())
bot.register_next_step_handler(message, del_task)
########################## ADMIN_TASKS ##############################
elif message.text == 'Все задачи ?':
# Просмотр всех задач статуса задачи
try:
connection = sqlite3.connect('./requests.db')
cursor = connection.cursor()
cursor.execute('SELECT * FROM request')
tasks = cursor.fetchall()
su = str('')
for task in tasks:
s1 = f' \t\t Обращение № {task[0]} \nОт: @{task[1]}. \nпроблема:\n{task[2]}\nСтатус: {task[3]}\n\n'
su = su + s1
s1 = ''
connection.close()
bot.send_message(chat_id=it_adm, text=f'{su}', parse_mode='HTML')
bot.send_message(chat_id=it_adm, text=f'Готово!', reply_markup=button.markup_task_l())
except Exception as e:
bot.send_message(chat_id=it_adm, text=f'Список пуст.', reply_markup=button.markup_task_l())
elif message.text == '❗️ Активные':
# Просмотр активных задач статуса задачи
try:
connection = sqlite3.connect('./requests.db')
cursor = connection.cursor()
cursor.execute('SELECT * FROM request')
tasks = cursor.fetchall()
ac = str('')
for task in tasks:
if task[3] == 'В работе':
a1 = f' \t\t Обращение № {task[0]} \nОт: @{task[1]}. \nпроблема:\n{task[2]}\nСтатус: {task[3]}\n\n'
ac = ac + a1
a1 = ''
else:
continue
connection.close()
bot.send_message(chat_id=it_adm, text=f'{ac}', parse_mode='HTML')
bot.send_message(chat_id=it_adm, text=f'Готово!', reply_markup=button.markup_task_l())
except Exception as e:
bot.send_message(chat_id=it_adm, text=f'Список пуст. Вы отлично поработали!', reply_markup=button.markup_task_l())
elif message.text == '✅ Выполненные':
connection = sqlite3.connect('./requests.db')
cursor = connection.cursor()
try:
connection = sqlite3.connect('./requests.db')
cursor = connection.cursor()
cursor.execute('SELECT * FROM request')
tasks = cursor.fetchall()
da = str('')
for task in tasks:
if task[3] != 'В работе':
d1 = f' \t\t Обращение № {task[0]} \nОт: @{task[1]}. \nпроблема:\n{task[2]}\nСтатус: {task[3]}\n\n'
da = da + d1
d1 = ''
else:
continue
connection.close()
bot.send_message(chat_id=it_adm, text=f'{da}', parse_mode='HTML')
bot.send_message(chat_id=it_adm, text=f'Готово!', reply_markup=button.markup_task_l())
except Exception as e:
bot.send_message(chat_id=it_adm, text=f'Список пуст.', reply_markup=button.markup_task_l())
elif message.text == '? admin menu':
bot.send_message(chat_id=it_adm, text='Вы вернулись в меню Администратора...', reply_markup=button.markup_admin())
# Функция удаления задачи
def del_task(message):
try:
if message.text in menu:
get_text_messages(message)
else:
connection = sqlite3.connect('./requests.db')
cursor = connection.cursor()
try:
del_id = int(message.text)
cursor.execute('SELECT * FROM request')
tasks = cursor.fetchall()
try:
for task in tasks:
if task[0] == del_id:
cursor.execute('DELETE FROM request WHERE id = ?', (del_id,))
connection.commit()
bot.send_message(chat_id=it_adm, text=f' \t Обращение № {task[0]} \nОт: {task[1]}. \nпроблема:\n{task[2]}\n Статус: {task[3]}\nУДАЛЕНО!✔️\n', reply_markup=button.markup_admin())
raise StopIteration
else:
continue
else:
bot.send_message(chat_id=it_adm, text='!Не правильно введен номер!\nНажмите "⛔️Удалить задачу⛔️" и введите номер.' , reply_markup=button.markup_admin())
except StopIteration:
pass
except (SyntaxError, ValueError):
bot.send_message(chat_id=it_adm, text='Вы не ввели номер...\nНажмите "⛔️Удалить задачу⛔️" и введите номер.', reply_markup=button.markup_admin())
connection.close()
except Exception as e:
bot.send_message(chat_id=it_adm, text='Произошла ошибка... Попробуйте снова.\nНажмите "⛔️Удалить задачу⛔️" и введите номер.', reply_markup=button.markup_admin())
# Функция изменения статуса задачи
def stat_task(message):
try:
if message.text in menu:
get_text_messages(message)
else:
connection = sqlite3.connect('./requests.db')
cursor = connection.cursor()
try:
stat_task_adm = int(message.text)
cursor.execute('SELECT * FROM request')
tasks = cursor.fetchall()
try:
for task in tasks:
if task[0] == stat_task_adm:
cursor.execute('UPDATE request SET status = ? WHERE id = ?', ('Выполнено!', stat_task_adm))
connection.commit()
bot.send_message(chat_id=it_adm, text=f' \t Обращение № {task[0]} \nОт: {task[1]}. \nпроблема:\n{task[2]}\n Статус: {task[3]}\n\n ?Статус ОБНОВЛЕН!?', reply_markup=button.markup_admin())
raise StopIteration
else:
continue
else:
bot.send_message(chat_id=it_adm, text=f'!Не правильно введен номер!\nНажмите "? Изменить статус" и введите номер.' , reply_markup=button.markup_admin())
except StopIteration:
pass
except (SyntaxError, ValueError):
bot.send_message(chat_id=it_adm, text=f'Вы не ввели номер...\nНажмите "? Изменить статус" и введите номер.', reply_markup=button.markup_admin())
connection.close()
except Exception as e:
bot.send_message(chat_id=it_adm, text=f'Произошла ошибка... Попробуйте снова.\nНажмите "? Изменить статус" и введите номер.', reply_markup=button.markup_admin())
# Функция вывода статуса задачи пользователю
def stat_user_tasks(message):
try:
if message.text in menu:
get_text_messages(message)
else:
connection = sqlite3.connect('./requests.db')
cursor = connection.cursor()
try:
nomber = int(message.text)
cursor.execute('SELECT * FROM request')
tasks = cursor.fetchall()
try:
for task in tasks:
if task[0] == nomber:
bot.send_message(message.chat.id, f' \t Обращение № {task[0]} \nОт: {task[1]}. \nпроблема:\n{task[2]}\n Статус: {task[3]}\n\n', reply_markup=button.markup_it())
raise StopIteration
else:
continue
else:
bot.send_message(message.chat.id,'!Не правильно введен номер!\nНажмите "⚠️ Проверить статус заявки" и введите номер.' , reply_markup=button.markup_it())
except StopIteration:
pass
except (SyntaxError, ValueError):
bot.send_message(message.chat.id,'Вы не ввели номер...\nНажмите "⚠️ Проверить статус заявки" и введите номер.', reply_markup=button.markup_it())
connection.close()
except Exception as e:
bot.send_message(message.chat.id, 'Произошла ошибка... Попробуйте снова.\nНажмите "⚠️ Проверить статус заявки" и введите номер.', reply_markup=button.markup_it())
#Функция внесения задачи пользователем
def add_task(message):
try:
if message.text in menu:
get_text_messages(message)
else:
connection = sqlite3.connect('./requests.db')
cursor = connection.cursor()
# Создаем таблицу request
cursor.execute('''
CREATE TABLE IF NOT EXISTS request (
id INTEGER PRIMARY KEY,
username TEXT,
example TEXT,
status TEXT DEFAULT 'В работе'
)
''')
cursor.execute('INSERT INTO request (username, example, status) VALUES (?, ?, ?)', (message.from_user.username, message.text, 'В работе'))
connection.commit()
last_id = cursor.lastrowid
connection.close()
bot.send_message(message.chat.id, f'Номер Вашего обращения: {last_id} \nСпасибо!\n\nПо номеру обращения Вы можете проверить статус.', reply_markup=button.markup_it())
bot.send_message(chat_id=it_adm, text=f'{message.from_user.first_name} оставил заявку: \n{message.text}\nNikname: @{message.from_user.username}', reply_markup=None)
except Exception as e:
bot.send_message(message.chat.id, 'Произошла ошибка... Попробуйте снова.\n\nНажмите кнопку "? Оставить заявку" и введите сообщение.', reply_markup=button.markup_it())
2.6 Запуск программы:
if __name__ == "__main__":
# bot.polling(none_stop=True, interval=0) # ранее использовалось.
while True:
try:
bot.polling(none_stop=True)
except Exception as e:
logger.error(e)
time.sleep(15)
3. Обзор кода button.py
В целом здесь пояснять особо нечего… Задаем функции, внутри которых описываем клавиатуру и задаем положение кнопок при выводе.
from telebot import types
def markup_start():
markup_start = types.ReplyKeyboardMarkup(resize_keyboard=True)
btn0 = types.KeyboardButton('?')
markup_start.row(btn0)
return markup_start
def markup_main():
markup_main = types.ReplyKeyboardMarkup(resize_keyboard=True)
btn1 = types.KeyboardButton('?')
btn2 = types.KeyboardButton('? Тех. поддержка')
btn3 = types.KeyboardButton('? Информация')
btn4 = types.KeyboardButton('❌CLOSE❌')
markup_main.row(btn1, btn2)
markup_main.row(btn3, btn4)
return markup_main
def markup_it():
markup_it = types.ReplyKeyboardMarkup(resize_keyboard=True)
btn1_it = types.KeyboardButton('⚠️ Проверить статус заявки')
btn2_it = types.KeyboardButton('? Оставить заявку')
btn3_it = types.KeyboardButton('? назад')
markup_it.row(btn1_it, btn2_it)
markup_it.row(btn3_it)
return markup_it
def markup_admin():
markup_adm = types.ReplyKeyboardMarkup(resize_keyboard=True)
btn1_adm= types.KeyboardButton('Списки задач?')
btn2_adm= types.KeyboardButton('? Изменить статус')
btn3_adm = types.KeyboardButton('⛔️Удалить задачу⛔️')
btn4_adm = types.KeyboardButton('? назад')
markup_adm.row(btn1_adm, btn2_adm)
markup_adm.row(btn3_adm, btn4_adm)
return markup_adm
def markup_task_l():
markup_adm = types.ReplyKeyboardMarkup(resize_keyboard=True)
btn1_adm= types.KeyboardButton('Все задачи ?')
btn2_adm= types.KeyboardButton('❗️ Активные')
btn3_adm = types.KeyboardButton('✅ Выполненные')
btn4_adm = types.KeyboardButton('? admin menu')
markup_adm.row(btn1_adm, btn2_adm)
markup_adm.row(btn3_adm, btn4_adm)
return markup_adm
def markup_secure():
markup_sec = types.ReplyKeyboardMarkup(resize_keyboard=True)
btn1_sec= types.KeyboardButton('Постановка на охрану ?♀️')
btn2_sec= types.KeyboardButton('Запрос на снятие с охраны ⚡️')
btn3_sec = types.KeyboardButton('? назад')
btn4_sec = types.KeyboardButton('❌CLOSE❌')
markup_sec.row(btn1_sec, btn2_sec)
markup_sec.row(btn3_sec, btn4_sec)
return markup_sec
def markup_up():
markup_up = types.ReplyKeyboardMarkup(resize_keyboard=True)
btn1_up= types.KeyboardButton('? Офис 1 ⚙️')
btn2_up= types.KeyboardButton('? Офис 2 ?')
btn3_up = types.KeyboardButton('? назад')
btn4_up = types.KeyboardButton('❌CLOSE❌')
markup_up.row(btn1_up, btn2_up)
markup_up.row(btn3_up, btn4_up)
return markup_up
def markup_down():
markup_down = types.ReplyKeyboardMarkup(resize_keyboard=True)
btn1_down= types.KeyboardButton('? Офис 1 ⚙️')
btn2_down= types.KeyboardButton('? Офис 2 ?')
btn3_down = types.KeyboardButton('? назад')
btn4_down = types.KeyboardButton('❌CLOSE❌')
markup_down.row(btn1_down, btn2_down)
markup_down.row(btn3_down, btn4_down)
return markup_down
4. Содержание .env
TOKEN='423....1:B......sd' # Токен бота в Telegram
OFFICE='-1000000000000' # id группы администрации офиса
IT_ADM='-1000000000001' # id группы IT отдела
SECURE='-1000000000002' # id группы безопасности
API_MTS_KEY='Bearer s...p' # api токен для доступа к МТС Exolve
NUMBER='79111111110' # номер телефона добавленного в личный кабинет МТС Exolve
NUMBER_1='79111111111' # номер телефона сим в блоке управления сигнализацией офис 1
NUMBER_2='79111111112' # номер телефона сим в блоке управления сигнализацией офис 2
Из данного файла переменные окружения будут вытягиваться в тело основной программы.
5. Описание start.sh
Для удобства было принято решение создать скрипт для запуска контейнера.
Данный скрипт имеет следующее содержание:
#!/usr/bin/bash # или /usr/bin/env bash
source /usr/src/app/.env # позволяет портировать переменные в текущую оболочку.
python /usr/src/app/main.py # запуск программы.
Стоит отметить, что не обладаю огромным опытом в использовании Docker, и этот способ который был придуман для переноса переменных окружения в контейнер. Есть иные описанные варианты в основном при использовании Docker Compose или явное определение переменных в Dockerfile.
6. Описание Dockerfile
Создадим файл Dockerfile. Со следующим содержанием:
FROM python:3.11 # Базовый образ на основе которого будет создан контейнер. Бот был написан на Python 3.11 по этому и используется данный образ.
LABEL developed_by='@LeoAlecksey' # метаданные полезной нагрузки не несут.
WORKDIR /usr/src/app # Выбор рабочей директории.
COPY ./requirements.txt . # Копируем файл с перечнем использованных в проекте пакетов.
RUN python -m pip install --upgrade pip # Обновляем пакетный менеджер pip.
RUN python -m pip install -r requirements.txt # Устанавливаем пакеты для проекта в соответствии с перечнем.
COPY ./main.py . # копируем файлы в проекта в контейнер.
COPY ./button.py .
COPY ./LICENCE.md .
COPY ./requests.db ./requests.db
COPY ./.env .
COPY ./.gitignore .
COPY ./start.sh .
CMD ["/bin/bash", "/usr/src/app/start.sh"] # команда которая будет выполнена при запуске контейнера.
7. Bonus
В момент добавления функционала по работе с сигнализацией, и тестирования контейнера приходилось пересобирать его не один раз. Решил попробовать автоматическую сборку, так как проект хранился в GitHub, один из вариантов GitHub Actions.
7.1 Создаем workflows
Создаем директорию .github/workflows и файл Prod_build_TG_bot.yml
Prod_build_TG_bot.yml — описывает рабочий процесс в GitHub Actions.
name: build_prod_Tg_bot # имя
on: # настройки триггера, в данном случае при пуше в ветку main будет запускаться процесс.
push:
branches:
- main
jobs:
## Сборка Docker-image с tg_bot Push on DockerHub ##
####################################################
docker_image_build_and_push:
name: Publish TG_bot to Docker Hub
runs-on: ubuntu-latest # базовый образ операционной системы в котором будут производиться действия по сборке.
steps:
- uses: actions/checkout@v4 # клонирует наш репозиторий в операционную систему.
with:
fetch-depth: 0
- name: Output Run Attempt # берем номер запуска сборки для версионирования.
run: echo ${{ github.run_number }}
- name: Login to Docker Hub # Логинимся на hub.docker.com
run: docker login -u ${{ secrets.DOCKER_NAME }} -p ${{ secrets.DOCKER_PASS }}
- name: Build Container image # создаем контейнер с тегом TG-Bot-v run_attempt - количество попыток запуска run_number - номер запуска сборки.
run: docker build -t ${{ secrets.DOCKER_REPO }}:TG-Bot-v${{ github.run_attempt }}.${{ github.run_number }} .
- name: Publish Docker image # пушим контейнер в docker hub
run: docker push ${{ secrets.DOCKER_REPO }}:TG-Bot-v${{ github.run_attempt }}.${{ github.run_number }}
GitHub, номер запуска сборки.
HubDocker, версионирование по номеру запуска сборки.
7.2 Добавляем переменные
Переменные:
DOCKER_NAME — логин для Docker hub.
DOCKER_PASS — пароль для Docker hub.
DOCKER_REPO — репозиторий Docker hub в который будет добавлен новый контейнер. Прим: leoalecksey/scripts
Переменные определяем в репозитории проекта на GitHub Settings — Secrets and varriables — Actions, далее Secrets — New Repository secret.
Главное меню
Меню обращений
Admin группа и меню
Статус обращения
Попытка перехода в меню сигнализации от пользователя
Постановка на охрану через группу Secure
Оповещение в группу администраторов офиса
Заключение
Мой путь в изучение Docker начался именно с простейшей упаковки данной программы, так как запуск контейнера Hello world! не дает даже малейшего понимания о работе технологии. Данная статья поможет в понимании и начале использования такого средства автоматизации как GitHub Actions.
Хотелось бы отметить, что реализация довольно простая, постепенно добавлялся новый функционал, но для понимания хватит базовых знаний Python.
Отдельная благодарность ребятам из MTS Exolve, за быстрое взаимодействие и действительно удобный сервис с отличной документацией!
P.S. не являюсь разработчиком, заранее прошу не кидать тапками, а поделиться опытом если нашли места где можно оптимизировать логику или код.
Буду рад обратной связи!
Комментарии (9)
omgiafs
22.12.2024 02:12Разве не правильнее и безопаснее при проверке принадлежности к админам вместо имени чата использовать его ID в этом коде? Иначе создаём чат с таким же title, как у админов, запускаем там бота и вуаля: мы - админ.
def admin(message): if message.chat.title == 'Chat Name IT':
Логично предположить, что затем можно упороться и создать БД юзеров с правами. Или подгружать из LDAP, но это уже Каневский.jpg
suprunchuk
22.12.2024 02:12конечно. я тоже не понял этого прикола.
user id уникальный для каждого аккаунта.. аchat.title
уязвимое место:@bot.message_handler(commands=['admin']) def admin(message): if str(message.chat.id) == it_adm: bot.send_message(chat_id=it_adm, text='Добрый день!\nВы в меню администратора.\nЧто Вам необходимо?\n', reply_markup=button.markup_admin()) else: bot.send_message(message.chat.id, 'Вы не являетесь администратором', reply_markup=button.markup_start())
LeoALecksey Автор
22.12.2024 02:12Спасибо за подробное описание уязвимости на GitHub, внес изменения в статье и на GitHub!
LeoALecksey Автор
22.12.2024 02:12Спасибо, согласен. Имеется в данном месте проблема, но при создании группы с таким же именем, ответы будут прилетать в группу админов а не в группу с таким же chat.name.
В любом случае поправить имеет смысл, так как имеет место уязвимость. И держится лишь на незнание оригинального имени группы.
Отмечу ещё момент, та же ситуация с проверкой группы Secure.
aleksxx
Классный готовый телеграм бот для службы технической поддержки практически любого сервиса. Вот бы прикрутить еще веб морду админки для удобной аналитики и обработки заявок. Добавил в закладки.