commit e5179c039bd87a3aca5f0a081e631690672c9e54 Author: L_DelOff Date: Thu Apr 18 16:25:22 2024 +0300 рефактор diff --git a/README.md b/README.md new file mode 100644 index 0000000..88f2dee --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# MumbleConciergeBot + +Бот, который оживит ваш сервер Mumble. Он следит за перемещением пользователей по каналам и отправляет сообщение. Так вы сделаете комнаты атмосфернее. + +Бот написан с использованием библиотеки [Pymumble](https://github.com/azlux/pymumble). Спасибо за это [@Azlux](https://github.com/azlux) + +Подробнее [здесь](https://github.com/LDelOff/MumbleConciergeBot/wiki) diff --git a/channels.csv b/channels.csv new file mode 100644 index 0000000..0856cbf --- /dev/null +++ b/channels.csv @@ -0,0 +1,39 @@ +path;message +Комнаты постояльцев;"

" +Комнаты постояльцев/CSGO;"

" +Комнаты постояльцев/Minecraft;"

" +Барная стойка;"

" +Гостинная;"

" +Стойка консьержа;"

 

+ + + + + + +
+

 Заказать встречу с aдминистратором  

+
+

" +Концертный зал;"

 

+ + + + + + +
+

 Заказать песню

+
+

" +Гостинная/Стол Эволюция;"

 

+ + + + + + +
+

 Создать игру или присоединиться

+
+

" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c7686fc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pymumble +pandas~=2.2.1 \ No newline at end of file diff --git a/settings.default.ini b/settings.default.ini new file mode 100644 index 0000000..013a104 --- /dev/null +++ b/settings.default.ini @@ -0,0 +1,18 @@ +[Server] +address = 127.0.0.1 +port = 64738 +password = +tokens = +[Bot] +bot_name = Харон +certfile = haron.pem +data_folder = bot_data +userlist = userlist.ini +admins = admin1;admin2; +[Command user] +help = !help - Вызывает это сообщение +send = !send !"имя пользователя"!"сообщение" - Отправить оффлайн сообщение +users = !users - Cписок лиц, кому можно оставить сообщение +[Command admin] +createfoldertree = !createfoldertree - создает в папке data_folder множество каталогов, соответсвующих своему каналу +deletefoldertree = !deletefoldertree - удаляет эту папку(ОСТОРОЖНО! не потеряйте, всё что сделали) \ No newline at end of file diff --git a/settings.ini b/settings.ini new file mode 100644 index 0000000..356413f --- /dev/null +++ b/settings.ini @@ -0,0 +1,18 @@ +[Server] +address = ldeloff.ru +port = 64738 +password = +tokens = +[Bot] +bot_name = Харон_test +certfile = harontest.pem +data_folder = bot_data +userlist = userlist.ini +admins = L_DelOff;L_DelOff_m; +[Command user] +help = !help - Вызывает это сообщение +send = !send !"имя пользователя"!"сообщение" - Отправить оффлайн сообщение +users = !users - Cписок лиц, кому можно оставить сообщение +[Command admin] +createfoldertree = !createfoldertree - создает в папке data_folder множество каталогов, соответсвующих своему каналу +deletefoldertree = !deletefoldertree - удаляет эту папку(ОСТОРОЖНО! не потеряйте, всё что сделали) \ No newline at end of file diff --git a/src/Utils.py b/src/Utils.py new file mode 100644 index 0000000..4a753ee --- /dev/null +++ b/src/Utils.py @@ -0,0 +1,6 @@ +import configparser + + +class Utils: + def __init__(self): + pass \ No newline at end of file diff --git a/src/config/BotConfig.py b/src/config/BotConfig.py new file mode 100644 index 0000000..39a1aae --- /dev/null +++ b/src/config/BotConfig.py @@ -0,0 +1,46 @@ +import configparser +import os +from typing import List +import pandas + +from src.model.MumbleChannel import MumbleChannel + + +class BotConfig: + config: configparser.ConfigParser + adminList: List + userList: configparser.ConfigParser + channelList: List[MumbleChannel] + + def __init__(self): + self.load_settings() + self.channelList = [] + self.load_channel_list() + + def load_settings(self): + self.config = configparser.ConfigParser() + self.config.read('settings.ini', encoding="utf-8") + self.adminList = self.config['Bot']['admins'].rsplit(sep=';') + + if not os.path.exists(self.config['Bot']['userlist']): + f = open(self.config['Bot']['userlist'], 'tw', encoding='utf-8') + f.close() + + self.userList = configparser.ConfigParser() + self.userList.read(self.config['Bot']['userlist']) + + def load_channel_list(self): + data = pandas.read_csv('channels.csv', delimiter=";") + data = data.reset_index() + for index, row in data.iterrows(): + self.channelList.append(MumbleChannel(row['path'], row['message'])) + + + + +# bot = BotConfig() +# config = bot.config +# adminList = bot.adminList +# userList = bot.userList +#for ch in bot.channelList: +# print(ch.__str__()) diff --git a/src/controller/MumbleController.py b/src/controller/MumbleController.py new file mode 100644 index 0000000..3eeb8a4 --- /dev/null +++ b/src/controller/MumbleController.py @@ -0,0 +1,52 @@ +import logging + +from src.model import MumbleBot +from src.service.MumbleBotService import MumbleBotService + +_logger = logging.getLogger(__name__) + + +class MumbleController: + @staticmethod + def message_received(bot_info: MumbleBot, message_info): + user_id = message_info.actor + if user_id == 0: + return + + username = bot_info.get_username_by_id(user_id) + is_admin = bot_info.is_admin(username) + message = message_info.message.strip() + + print(f"{username} {('', '(админ бота)')[is_admin]} прислал сообщение: {message}") + _logger.info(f"{username} {('', '(админ бота)')[is_admin]} прислал сообщение: {message}") + + match message: + case '!help': + message = ' ' + for x in bot_info.config.config['Command user']: + message = message + '

' + bot_info.config.config['Command user'][x] + '

' + if is_admin: + for x in bot_info.config.config['Command admin']: + message = message + '

' + bot_info.config.config['Command admin'][x] + '

' + bot_info.get_user_by_id(user_id).send_text_message(message) + case '!users': + if is_admin: + message = '

Список пользователей:

' + users = bot_info.get_userlist() + for user in users: + message = message + '

' + user['name'] + '

' + bot_info.get_user_by_id(user_id).send_text_message(message) + case '!initchannels': + MumbleBotService.init_channels(bot_info) + message = 'Каналы обновлены' + bot_info.get_user_by_id(user_id).send_text_message(message) + case 'test': + print('тестовая команда') + MumbleBotService.test(bot_info.bot, message_info) + case _: + bot_info.get_user_by_id(user_id).send_text_message(message) + + @staticmethod + def user_change_channel(bot_info: MumbleBot, user, action): + if 'channel_id' in user: # колбэк вызывается также включением/выключением микрофона у пользователя, тут стоит фильтр "Только переходы по каналам" + MumbleBotService.user_change_channel(bot_info, user, action) diff --git a/src/model/MumbleBot.py b/src/model/MumbleBot.py new file mode 100644 index 0000000..d669174 --- /dev/null +++ b/src/model/MumbleBot.py @@ -0,0 +1,111 @@ +import logging +import time +from typing import Optional + +import pymumble_py3.constants +from pymumble_py3 import Mumble +from pymumble_py3.users import Users, User + +from src.config.BotConfig import BotConfig +from pymumble_py3.callbacks import PYMUMBLE_CLBK_TEXTMESSAGERECEIVED as PCTMR +from pymumble_py3.callbacks import PYMUMBLE_CLBK_USERUPDATED as PCUU + + +from src.controller.MumbleController import MumbleController +from src.service.MumbleBotService import MumbleBotService + +_logger = logging.getLogger(__name__) + + +class MumbleBot: + config: BotConfig + bot: Mumble + + def __init__(self): + self.thread = None + self._loop_status = None + + self.exit = None + _logger.info('!!!Starting bot!!!') + self.config = BotConfig() + config = self.config.config + self.bot = Mumble(host=config['Server']['address'], user=config['Bot']['Bot_name'], + port=int(config['Server']['port']), password=config['Server']['password'], + certfile=config['Bot']['certfile'], reconnect=True, tokens=config['Server']['tokens']) + + self.bot.callbacks.set_callback(PCTMR, self.message_received) + self.bot.callbacks.set_callback(PCUU, self.user_change_channel) + # self.bot.callbacks.set_callback(PCUC, self.user_connect_server) + + self.bot.start() # start the mumble thread + self.bot.is_ready() # wait for the connection + if self.bot.connected >= pymumble_py3.constants.PYMUMBLE_CONN_STATE_FAILED: + exit() + + def message_received(self, message_info): + MumbleController.message_received(self, message_info) + + def user_change_channel(self, user, action): + MumbleController.user_change_channel(self, user, action) + + def get_username_by_id(self, user_id: int) -> Optional[str]: + try: + return self.get_user_by_id(user_id)['name'] + except KeyError: + _logger.warning(f"Не удалось найти пользователя с id: {user_id}") + return None + + def get_user_by_id(self, user_id: int) -> Optional[User]: + try: + users = self.bot.users + for user in users.values(): + if user['user_id'] == user_id: + return user + _logger.warning(f"Не удалось найти пользователя с id: {user_id}") + return None + except KeyError: + _logger.warning(f"Не удалось найти пользователя с id: {user_id}") + return None + + def get_user_by_name(self, user_name: str) -> Optional[User]: + users = self.bot.users + for user in users: + if user['name'] == user_name: + return user + _logger.warning(f"Не удалось найти пользователя с username: {user_name}") + return None + + def get_userlist(self) -> Users: + return self.bot.users.values() + + # # TODO + # # обработчик колбэка подключения пользователя к серверу + # @staticmethod + # def user_connect_server(user): + # if kostyl1 == 1: + # # дополняем список всех пользователей сервера + # if mumble.users[user['session']]['name'] in userlist: + # welcome_message(user) + # check_offline_messages(user) + # else: + # first_welcome_message(user) + # userlist.add_section(mumble.users[user['session']]['name']) + # userlist.set(mumble.users[user['session']]['name'], mumble.users[user['session']]['name'], '') + # with open(config['Bot']['userlist'], "w") as config_file: + # userlist.write(config_file) + + def is_admin(self, user) -> bool: + list_admin = self.config.adminList + if user in list_admin: + return True + else: + return False + + def loop(self): + MumbleBotService.init_channels(self) + while not self.exit and self.bot.is_alive(): + + while self.thread and self.bot.sound_output.get_buffer_size() > 0.5 and not self.exit: + # If the buffer isn't empty, I cannot send new music part, so I wait + self._loop_status = f'Wait for buffer {self.bot.sound_output.get_buffer_size():.3f}' + time.sleep(0.01) diff --git a/src/model/MumbleChannel.py b/src/model/MumbleChannel.py new file mode 100644 index 0000000..9afecc4 --- /dev/null +++ b/src/model/MumbleChannel.py @@ -0,0 +1,13 @@ +class MumbleChannel: + name: str + path: str + message: str + + def __init__(self, path: str, message: str): + self.path = path + self.message = message + self.name = path.split('/')[-1] + + def __str__(self) -> str: + string = f"MumbleChannel:\n name: {self.name} \n path: {self.path} \n message: {self.message}" + return string diff --git a/src/service/ChannelService.py b/src/service/ChannelService.py new file mode 100644 index 0000000..a97cca6 --- /dev/null +++ b/src/service/ChannelService.py @@ -0,0 +1,48 @@ +import logging +from time import sleep + +from pymumble_py3 import Mumble +from src.model import MumbleBot + +_logger = logging.getLogger(__name__) + + +class ChannelService: + @classmethod + def init_channels(cls, mumbleBot: MumbleBot): + channels = mumbleBot.config.channelList + for channel in channels: + parent_channel_id = 0 + path_split = channel.path.split('/') + for path in path_split: + parent_channel_id = cls.create_channel(mumbleBot.bot, path, parent_channel_id) + + @classmethod + def create_channel(cls, mumbleBot: Mumble, name: str, parent_channel_id: int) -> int: + parent_channel_obj = [] + channels = mumbleBot.channels.values() + for item in channels: + if item.get('channel_id') is not None and item["channel_id"] == parent_channel_id: + parent_channel_obj = item + if item.get('parent') is not None and item["parent"] == parent_channel_id and item["name"] == name: + return item["channel_id"] + + _logger.info(f"Создаю подканал {name} в {parent_channel_obj['name']}") + mumbleBot.channels.new_channel(parent_channel_id, name, temporary=False) + sleep(1) + + channels = mumbleBot.channels.values() + for item in channels: + if item.get('parent') is not None and item["parent"] == parent_channel_id and item["name"] == name: + return item["channel_id"] + + return 0 + + @staticmethod + def get_message_by_channel_id(mumbleBot: MumbleBot, channel_id: int) -> str: + name = mumbleBot.bot.channels[channel_id]['name'] + channel_list = mumbleBot.config.channelList + for channel in channel_list: + if channel.name == name: + return channel.message + return '' diff --git a/src/service/MumbleBotService.py b/src/service/MumbleBotService.py new file mode 100644 index 0000000..4b96f93 --- /dev/null +++ b/src/service/MumbleBotService.py @@ -0,0 +1,29 @@ +import logging + +from pymumble_py3 import Mumble + +from src.model import MumbleBot +from src.service.ChannelService import ChannelService + +_logger = logging.getLogger(__name__) + + +class MumbleBotService: + + @staticmethod + def init_channels(bot_info): + ChannelService.init_channels(bot_info) + + # TODO: одинаковые каналы (коллизия сообщений) + @staticmethod + def user_change_channel(bot_info: MumbleBot, user, action): + _logger.info(f"{user['name']} перешёл в канал: { bot_info.bot.channels[action['channel_id']]['name']}") + print(f"{user['name']} перешёл в канал: { bot_info.bot.channels[action['channel_id']]['name']}") + + message = ChannelService.get_message_by_channel_id(bot_info, action['channel_id']) + if len(message) > 0: + bot_info.get_user_by_id(user['user_id']).send_text_message(message) + + @staticmethod + def test(bot: Mumble, message_info): + ChannelService.get_tree(bot) diff --git a/src/start.py b/src/start.py new file mode 100644 index 0000000..4c6f93b --- /dev/null +++ b/src/start.py @@ -0,0 +1,8 @@ +import logging + +from src.model.MumbleBot import MumbleBot + +logging.getLogger().setLevel(logging.INFO) +bot = MumbleBot() +bot.loop() + diff --git a/start_bot.py b/start_bot.py new file mode 100644 index 0000000..5c7b4b0 --- /dev/null +++ b/start_bot.py @@ -0,0 +1,148 @@ +import pymumble_py3 +import time +import os +import shutil +import configparser + +from pymumble_py3.callbacks import PYMUMBLE_CLBK_TEXTMESSAGERECEIVED as PCTMR +from pymumble_py3.callbacks import PYMUMBLE_CLBK_USERUPDATED as PCUU +from pymumble_py3.callbacks import PYMUMBLE_CLBK_USERCREATED as PCUC + +kostyl1 = 0 + + +# функция создаёт папки, в которых содержатся сообщения для тех, кто зашёл на канал +def create_folder_tree(): + path = config['Bot']['data_folder'] + "/" + try: + os.mkdir(path) + except OSError: + print("Создать директорию %s не удалось" % path) + else: + print("Успешно создана директория %s " % path) + + path2 = path + 'welcomemessage.txt' + try: + f = open(path2, 'tw', encoding='utf-8') + f.close() + except OSError: + print("Создать файл %s не удалось" % path2) + else: + print("Успешно создан файл %s " % path2) + path2 = path + 'firstwelcomemessage.txt' + try: + f = open(path2, 'tw', encoding='utf-8') + f.close() + except OSError: + print("Создать файл %s не удалось" % path2) + else: + print("Успешно создан файл %s " % path2) + for x in mumble.channels: + # определим имя директории, которую создаём + path3 = path + str(x) + '.' + mumble.channels[x]['name'] + try: + os.mkdir(path3) + except OSError: + print("Создать директорию %s не удалось" % path3) + else: + print("Успешно создана директория %s " % path3) + path4 = path + str(x) + '.' + mumble.channels[x]['name'] + '/tm_' + mumble.channels[x]['name'] + '.txt' + try: + f = open(path4, 'tw', encoding='utf-8') + f.close() + except OSError: + print("Создать файл %s не удалось" % path4) + else: + print("Успешно создан файл %s " % path4) + + +# функция удаляет папки +def delete_folder_tree(): + path = config['Bot']['data_folder'] + "/" + try: + shutil.rmtree(path) + except OSError: + print("Удалить директорию %s не удалось" % path) + else: + print("Успешно удалена директория %s" % path) + path = "bot_data/" + try: + os.mkdir(path) + except OSError: + print("Создать директорию %s не удалось" % path) + else: + print("Успешно создана директория %s " % path) + + +# функция отправляет сообщение пользователю, зашедшему на канал +def send_message_to_user(channel_id, actor): + path = config['Bot']['data_folder'] + "/" + str(channel_id) + '.' + mumble.channels[channel_id]['name'] + '/tm_' + \ + mumble.channels[channel_id]['name'] + '.txt' + try: + f = open(path, 'r', encoding="utf-8") + text = f.read() + mumble.users[actor].send_text_message(text) + f.close() + except OSError: + print('Сообщение не отправлено') + else: + print('Сообщение отправлено ' + mumble.users[actor]['name'] + ':' + text) + + +# функция отправляет сообщение пользователю, зашедшему на сервер +def welcome_message(user): + path = config['Bot']['data_folder'] + '/welcomemessage.txt' + try: + f = open(path, 'r', encoding="utf-8") + text = f.read() + mumble.users[user['session']].send_text_message(text) + f.close() + except OSError: + print('Приветствие не отправлено') + else: + print('Приветствие отправлено ' + mumble.users[user['session']]['name'] + ':' + text) + + +# функция отправляет сообщение пользователю, зашедшему на сервер впервые +def first_welcome_message(user): + path = config['Bot']['data_folder'] + '/firstwelcomemessage.txt' + try: + f = open(path, 'r', encoding="utf-8") + text = f.read() + mumble.users[user['session']].send_text_message(text) + f.close() + except OSError: + print('Приветствие не отправлено') + else: + print('Приветствие отправлено ' + mumble.users[user['session']]['name'] + ':' + text) + + +# пользователь "проверяет" свой почтовый ящик +def check_offline_messages(user): + for x in userlist[mumble.users[user['session']]['name']]: + if userlist[mumble.users[user['session']]['name']][x] != '': + text = '

Пользователь ' + x + ' оставил сообщение:

' + '

' + \ + userlist[mumble.users[user['session']]['name']][x] + '

' + mumble.users[user['session']].send_text_message(text) + userlist.set(mumble.users[user['session']]['name'], x, '') + with open(config['Bot']['userlist'], "w") as config_file: + userlist.write(config_file) + print('Оффлайн сообщение отправлено ' + mumble.users[user['session']]['name'] + ':' + text) + + + + + +# говорим как подключиться боту к серверу + +# к колбэкам прикручиваем функции-обработчики + +# подключаем бота к серверу +mumble.start() +# костыль +time.sleep(5) +print('костыль функционирует') +kostyl1 = 1 +# циклим всё, чтобы бот не отключался +while 1: + time.sleep(1) diff --git a/userlist.ini b/userlist.ini new file mode 100644 index 0000000..f11bb03 --- /dev/null +++ b/userlist.ini @@ -0,0 +1,17 @@ +[L_DelOff_m] +l_deloff_m = +l_deloff = + +[Arkan] +l_deloff = +l_deloff_m = + +[] + = + +[L_DelOff] +l_deloff = + +[] + = +