diff --git a/README.md b/README.md index ee9a60b..466b425 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Мой личный бот на python для ВК #### от dan63047 -Этот бот просто отвечает на поддержваемые запросы в переписке с сообществом. Был написан мною в целях изучения python +Этот бот просто отвечает на поддержваемые запросы в переписке с сообществом. Был написан мною в целях изучения python. Этот README является инструкцией по установке и использованию бота. # Функции бота Чтобы воспользоваться функциями бота необходимо отправить команду и в некоторых случаях агрумент. Все команды начинаются с восклицательного знака @@ -64,9 +64,9 @@ mysql_db = 'pythonanywhere_nickname$default' # Название вашей БД. ## Если вы решите использовать MySQL БД Вам необходимо будет купить тариф на pythonanywhere, где есть поддержка MySQL базы данных, с которой бот будет работать. Тыкаем на *Databases* в шапке сайта. Создаём пароль для вашей базы данных. По умолчанию будет создана база данных с названием `your_nickname$default`, но вы можете создать БД со своим названием. ## Запуск бота -Если аккаунт платный: в pythonanywhere тыкаем на *Tasks*, создаём и запускаем в *Always-on tasks* задачу `python3.8 /home/pythonanywhere_nickname/dan63047pythonbot/dan63047bot.py`, где вместо pythonanywhere_nickname должен быть ваш никнейм на pythonanywhere. После небольшого тупления pythonanywhere запустит вашего бота. +Если аккаунт платный: в pythonanywhere тыкаем на *Tasks*, создаём и запускаем в *Always-on tasks* задачу `python3.8 /home/pythonanywhere_nickname/dan63047pythonbot/longpulling.py`, где вместо pythonanywhere_nickname должен быть ваш никнейм на pythonanywhere. После небольшого тупления pythonanywhere запустит вашего бота. -Если аккаунт бесплатный: в bash консоли переходим в директорию бота и прописываем `python3 dan63047bot.py`. Консоль можно оставить в покое, но лучше раз в день проверять её чтобы консоль не отключили +Если аккаунт бесплатный: в bash консоли переходим в директорию бота и прописываем `python3 longpulling.py`. Консоль можно оставить в покое, но лучше раз в день проверять её чтобы консоль не отключили ## Другие варианты запуска бота Скорее всего, вы можете найти где-нибудь VPS или огранизовать всё на своём компьютере. Там вы просто скачиваете и устанавливаете Python последней версии, настраиваете конфиг бота и запускаете его. # botconsole.py @@ -75,6 +75,14 @@ mysql_db = 'pythonanywhere_nickname$default' # Название вашей БД. * **exit** — завершает работу консоли * **message *peer_id* *message*** — отправляет *message* в переписку *peer_id* от имени сообщества * **msg_history *peer_id* *count*** — возвращает историю переписки, последние *count* сообщений. Если *count* не указано, то последние 20 сообщений +На следующей команде хотелось бы уделить особое внимание, ведь с помощью неё можно вызывать функции бота +## Команда **bot** +Если просто вызвать эту команду, то она покажет список существующих объектов ботов. Однако так же у команды есть и большое количество аргументов: +* **bot *update*** — обновляет список обьектов ботов и их состояние +* **bot *create* *peer_id*** — создаёт обьект бота для переписки *peer_id* +* **bot *delete* *peer_id*** — удаляет существующий обьект бота для переписки *peer_id* +* **bot *changeMidnightFlag* *peer_id*** — меняет флаг в обьекте бота *peer_id*, по которому он определяет, надо ли оповещать о Midnight +* **bot *midnight* *peer_id*** — инициирует ивент Midnight для обьекта бота *peer_id* # Использованные библиотеки * [vk_api](https://github.com/python273/vk_api) — модуль для создания скриптов для социальной сети Вконтакте * [pyowm](https://github.com/csparpa/pyowm) — модуль для получения погоды через OpenWeather API diff --git a/botconsole.py b/console.py similarity index 62% rename from botconsole.py rename to console.py index 9d82f5e..4f59fab 100644 --- a/botconsole.py +++ b/console.py @@ -1,18 +1,13 @@ import vk_api import datetime import time -import requests import logging -import pyowm import random import json import threading -import pymysql -import vk_api -import wikipediaapi as wiki import config +import dan63047VKbot from pymysql.cursors import DictCursor -from pyowm.utils.config import get_default_config from collections import deque from vk_api.bot_longpoll import VkBotLongPoll, VkBotEventType @@ -37,7 +32,6 @@ def log(warning, text): logging.info(msg) print(msg) -log(False, "Script started") try: vk = vk_api.VkApi(token=config.vk_group_token) longpoll = VkBotLongPoll(vk, config.group_id) @@ -46,8 +40,10 @@ except Exception as e: log(True, "Can't connect to longpull: "+str(e)) exit(log(False, "[SHUTDOWN]")) + def cycle(): active = True + dan63047VKbot.load_users() print("Welcome to dan63047bot console\nEnter command:") while active: command = input(">") @@ -81,7 +77,7 @@ def cycle(): log(True, "No arguments") continue try: - if 1 in command[1]: + if len(command[1]) == 2: m = vk.method('messages.getHistory', {'count': command[1][1], 'peer_id': command[1][0]}) else: m = vk.method('messages.getHistory', {'peer_id': command[1][0]}) @@ -93,7 +89,7 @@ def cycle(): user_info = vk.method('users.get', {'user_ids': i["from_id"], 'fields': 'verified,last_seen,sex'}) f = f'{user_info[0]["first_name"]} {user_info[0]["last_name"]}' datetime_time = datetime.datetime.fromtimestamp(i['date']) - date = datetime_time.strftime('%d.%m.%Y в %H:%M:%S') + date = datetime_time.strftime('%d.%m.%Y %H:%M:%S') msg = f"{f} {date}: {i['text']}" if i['attachments']: for m in i['attachments']: @@ -110,6 +106,51 @@ def cycle(): print(msg) except Exception as e: log(True, f'Failed to get history: {str(e)}') + elif command[0] == "bot": + if len(command) == 2: + command[1] = command[1].split(' ', 1) + if len(command[1]) == 2: + if command[1][0] == "midnight": + try: + userinfo = dan63047VKbot.db.get_from_users(command[1][1]) + log(False, "Midnight flag: "+str(userinfo["midnight"])) + dan63047VKbot.bot[int(command[1][1])].event("midnight") + except KeyError as e: + log(True, "Bot object is not exist: "+str(e)) + elif command[1][0] == "changeMidnightFlag": + try: + userinfo = dan63047VKbot.db.get_from_users(command[1][1]) + if userinfo["midnight"]: + dan63047VKbot.bot[int(command[1][1])].change_flag('midnight', False) + else: + dan63047VKbot.bot[int(command[1][1])].change_flag('midnight', True) + userinfo = dan63047VKbot.db.get_from_users(command[1][1]) + log(False, "Midnight flag: "+str(userinfo["midnight"])) + except KeyError as e: + log(True, "Bot object is not exist: "+str(e)) + elif command[1][0] == "create": + try: + dan63047VKbot.create_new_bot_object(command[1][1]) + log(False, "Bot-object has been created") + except Exception as e: + log(True, "Can't create bot-object - "+str(e)) + elif command[1][0] == "delete": + try: + del dan63047VKbot.bot[int(command[1][1])] + dan63047VKbot.db.delete_user(command[1][1]) + log(False, "Bot-object has been deleted") + except Exception as e: + log(True, "Can't delete bot-object - "+str(e)) + else: + if command[1][0] == "update": + dan63047VKbot.load_users() + else: + if dan63047VKbot.bot: + log(False, f"There is {len(dan63047VKbot.bot)} bot-objects") + for i in dan63047VKbot.bot: + print(dan63047VKbot.bot[i]) + else: + print("No bot-objects") else: log(True, "Unknown command. Type 'help' for command list") diff --git a/dan63047bot.py b/dan63047VKbot.py similarity index 87% rename from dan63047bot.py rename to dan63047VKbot.py index 55ed53d..2c46589 100644 --- a/dan63047bot.py +++ b/dan63047VKbot.py @@ -1,3 +1,4 @@ +"""Here you can found Bot class and Database worker class""" import vk_api import datetime import time @@ -8,7 +9,6 @@ import random import json import threading import pymysql -import vk_api import wikipediaapi as wiki import config from pymysql.cursors import DictCursor @@ -41,8 +41,10 @@ def log(warning, text): logging.info(msg) print(msg) +bot = {} +errors_array = {"access": "Отказано в доступе", + "miss_argument": "Отсуствует аргумент", "command_off": "Команда отключена"} -log(False, "Script started") try: vk = vk_api.VkApi(token=config.vk_group_token) longpoll = VkBotLongPoll(vk, config.group_id) @@ -86,11 +88,6 @@ except Exception: weather_command = False log(True, "Invalid OpenWeatherMap API key, !weather command will be turned off") -bot = {} -errors_array = {"access": "Отказано в доступе", - "miss_argument": "Отсуствует аргумент", "command_off": "Команда отключена"} - - class Database_worker(): def __init__(self): @@ -278,13 +275,32 @@ def get_weather(place): class VkBot: + """Bot object, which can answer to user commands\n\n - def __init__(self, peer_id, midnight=False, awaiting=None, access=1, new_post=False, admin_mode=False): + Keyword arguments:\n + peer_id -- id of conversation with user for answering. Int\n + midnight -- flag of midnight function, which send every midnigtht a message. Defalt: False. Bool\n + awaiting -- strind, what show, which function awaiting input. Defalt: None. Str\n + access -- flag, what set access level to bot functions. Defalt: True. Bool\n + new_post -- flag of notificaton function about new post on group. Defalt: False. Bool\n + admin_mode -- flag of moderating function, which moderate conversation. Defalt: False. Bool + """ + + def __init__(self, peer_id, midnight=False, awaiting=None, access=True, new_post=False, admin_mode=False): + """Initialise the bot object\n\n + Keyword arguments:\n + peer_id -- id of conversation with user for answering. Int\n + midnight -- flag of midnight function, which send every midnigtht a message. Defalt: False. Bool\n + awaiting -- strind, what show, which function awaiting input. Defalt: None. Str\n + access -- flag, what set access level to bot functions. Defalt: True. Bool\n + new_post -- flag of notificaton function about new post on group. Defalt: False. Bool\n + admin_mode -- flag of moderating function, which moderate conversation. Defalt: False. Bool + """ log(False, f"[BOT_{peer_id}] Created new bot-object") self._CHAT_ID = peer_id self._AWAITING_INPUT_MODE = awaiting - self._ACCESS_LEVEL = access + self._ACCESS_TO_ALL = access self._SET_UP_REMINDER = {"task": None, "time": None} self._MIDNIGHT_EVENT = midnight self._NEW_POST = new_post @@ -299,7 +315,7 @@ class VkBot: "!echo", "!game", "!debug", "!midnight", "!access", "!turnoff", "!reminder", "!subscribe", "!random", "!admin_mode"] def __str__(self): - return f"[BOT_{str(self._CHAT_ID)}] a: {str(self._ACCESS_LEVEL)}, mn: {str(self._MIDNIGHT_EVENT)}, await: {str(self._AWAITING_INPUT_MODE)}, sub: {str(self._NEW_POST)}, adm: {str(self._ADMIN_MODE)}" + return f"[BOT_{str(self._CHAT_ID)}] a: {str(self._ACCESS_TO_ALL)}, mn: {str(self._MIDNIGHT_EVENT)}, await: {str(self._AWAITING_INPUT_MODE)}, sub: {str(self._NEW_POST)}, adm: {str(self._ADMIN_MODE)}" def __del__(self): log(False, f"[BOT_{str(self._CHAT_ID)}] Bot-object has been deleted") @@ -435,7 +451,7 @@ class VkBot: respond['text'] = errors_array["miss_argument"] elif message[0] == self._COMMANDS[11]: - if self._ACCESS_LEVEL or int(user_id) == int(config.owner_id): + if self._ACCESS_TO_ALL or int(user_id) == int(config.owner_id): try: respond['text'] = self.debug(message[1]) except IndexError: @@ -444,14 +460,14 @@ class VkBot: respond["text"] = errors_array["access"] elif message[0] == self._COMMANDS[12]: - if self._ACCESS_LEVEL or int(user_id) == int(config.owner_id): + if self._ACCESS_TO_ALL or int(user_id) == int(config.owner_id): if self._MIDNIGHT_EVENT: - self.change_midnight(False) + self.change_flag('midnight', False) self.send("Уведомление о миднайте выключено") log(False, f"[BOT_{self._CHAT_ID}] Unsubscribed from event \"Midnight\"") else: - self.change_midnight(True) + self.change_flag('midnight', True) self.send("Бот будет уведомлять вас о каждом миднайте") log(False, f"[BOT_{self._CHAT_ID}] Subscribed on event \"Midnight\"") @@ -463,16 +479,16 @@ class VkBot: try: if message[1] == "owner": respond['text'] = "Теперь некоторыми командами может пользоваться только владелец бота" - self.change_access(0) + self.change_flag('admin_mode', False) elif message[1] == "all": respond['text'] = "Теперь все могут пользоваться всеми командами" - self.change_access(1) + self.change_flag('admin_mode', True) else: respond['text'] = "Некорректный аргумент" except IndexError: respond['text'] = errors_array["miss_argument"] log(False, - f"[BOT_{self._CHAT_ID}] Access level changed on {self._ACCESS_LEVEL}") + f"[BOT_{self._CHAT_ID}] Access level changed on {self._ACCESS_TO_ALL}") else: respond['text'] = errors_array["access"] @@ -485,14 +501,14 @@ class VkBot: respond['text'] = "Функция удалена за ненадобнастью" elif message[0] == self._COMMANDS[16]: - if self._ACCESS_LEVEL or int(user_id) == int(config.owner_id): + if self._ACCESS_TO_ALL or int(user_id) == int(config.owner_id): if self._NEW_POST: - self.change_new_post(False) + self.change_flag('new_post', False) self.send("Уведомление о новом посте выключено") log(False, f"[BOT_{self._CHAT_ID}] Unsubscribed from new posts") else: - self.change_new_post(True) + self.change_flag('new_post', True) self.send( "Бот будет уведомлять вас о каждом новом посте") log(False, @@ -522,12 +538,12 @@ class VkBot: "peer_id": int(self._CHAT_ID), "group_id": config.group_id}) if self._ADMIN_MODE: respond["text"] = "Режим модерирования выключен" - self.change_admin_mode(False) + self.change_flag('admin_mode', False) log(False, f"[BOT_{self._CHAT_ID}] Admin mode: {self._ADMIN_MODE}") else: respond["text"] = "Режим модерирования включён" - self.change_admin_mode(True) + self.change_flag('admin_mode', True) log(False, f"[BOT_{self._CHAT_ID}] Admin mode: {self._ADMIN_MODE}") except Exception: @@ -761,26 +777,41 @@ class VkBot: return f"Рандомное число от {lower} до {higher}:
{r}" def change_await(self, awaiting=None): + """Change the awaiting input state + + Keyword arguments: + awaiting -- name of function, what awaiting input from user. Defalt: None. String + """ self._AWAITING_INPUT_MODE = awaiting db.update_user(self._CHAT_ID, "awaiting", self._AWAITING_INPUT_MODE) - def change_access(self, level): - self._ACCESS_LEVEL = level - db.update_user(self._CHAT_ID, "access", self._ACCESS_LEVEL) - - def change_new_post(self, new_post): - self._NEW_POST = new_post - db.update_user(self._CHAT_ID, "new_post", self._NEW_POST) - - def change_midnight(self, midnight): - self._MIDNIGHT_EVENT = midnight - db.update_user(self._CHAT_ID, "midnight", self._MIDNIGHT_EVENT) - - def change_admin_mode(self, admin_mode): - self._ADMIN_MODE = admin_mode - db.update_user(self._CHAT_ID, "admin_mode", self._ADMIN_MODE) + def change_flag(self, flag, value): + """Change 'flag' to 'value' + + Keyword arguments: + flag -- name of flag. Can be 'access', 'new_post', 'midnight', 'admin_mode'. String + value -- set the flag state. Bool + """ + if flag == 'access': + self._ACCESS_TO_ALL = value + db.update_user(self._CHAT_ID, "access", self._ACCESS_TO_ALL) + elif flag == 'new_post': + self._NEW_POST = value + db.update_user(self._CHAT_ID, "new_post", self._NEW_POST) + elif flag == 'midnight': + self._MIDNIGHT_EVENT = value + db.update_user(self._CHAT_ID, "midnight", self._MIDNIGHT_EVENT) + elif flag == 'admin_mode': + self._ADMIN_MODE = value + db.update_user(self._CHAT_ID, "admin_mode", self._ADMIN_MODE) def send(self, message=None, attachment=None): + """Send to user something. + + Keyword arguments: + message -- text of message. string + attachment -- name of attachment. string + """ try: random_id = random.randint(-9223372036854775808, 9223372036854775807) @@ -792,80 +823,3 @@ class VkBot: debug_array['messages_answered'] += 1 except Exception as e: log(True, f'Failed to send message: {str(e)}') - - -def bots(): - log(False, "Started listening longpull server") - debug_array['start_time'] = time.time() - for event in MyVkLongPoll.listen(longpoll): - try: - if event.type == VkBotEventType.MESSAGE_NEW: - log_msg = f'[MESSAGE] id: {event.message.id}, peer_id: {event.message.peer_id}, user_id: {event.message.from_id}' - if event.message.action: - log_msg += f', action: '+event.message.action["type"]+', user id in action: '+ str(event.message.action["member_id"]) - if event.message.text != "": - log_msg += f', text: "{event.message.text}"' - if event.message.attachments: - atch = ', attachments: ' - for i in event.message.attachments: - if i['type'] == "sticker": - atch += f"sticker_id{i[i['type']]['sticker_id']}" - elif i['type'] == "wall": - atch += i['type'] + str(i[i['type']]['from_id']) + \ - "_" + str(i[i['type']]['id']) + " " - elif i['type'] == "link": - atch += i['type'] + " " + i[i['type']]['title'] + " " - else: - atch += i['type'] + str(i[i['type']]['owner_id']) + \ - "_" + str(i[i['type']]['id']) + " " - log_msg += atch - log(False, log_msg) - debug_array['messages_get'] += 1 - if int(event.message.peer_id) in bot: - bot[event.message.peer_id].get_message(event) - else: - create_new_bot_object(event.message.peer_id) - bot[event.message.peer_id].get_message(event) - elif event.type == VkBotEventType.WALL_POST_NEW: - log(False, f"[NEW_POST] id{event.object.id}") - users = db.get_all_users() - for i in users: - if (config.use_database): - bot[int(i['chat_id'])].event("post", event.object) - else: - bot[int(i)].event("post", event.object) - elif event.type == VkBotEventType.MESSAGE_DENY: - log(False, - f"User {event.object.user_id} deny messages from that group") - del bot[int(event.object.user_id)] - db.delete_user(event.object.user_id) - else: - log(False, f"Event {str(event.type)} happend") - except Exception as kek: - log(True, f"Беды с ботом: {str(kek)}") - debug_array['bot_warnings'] += 1 - continue - - -def midnight(): - while True: - current_time = time.time()+10800 - if int(current_time) % 86400 == 0: - log(False, "[EVENT_STARTED] \"Midnight\"") - users = db.get_all_users() - for i in users: - if (config.use_database): - bot[int(i['chat_id'])].event("midnight") - else: - bot[int(i)].event("midnight") - log(False, "[EVENT_ENDED] \"Midnight\"") - time.sleep(1) - else: - time.sleep(0.50) - - -load_users() -tread_bots = threading.Thread(target=bots) -tread_midnight = threading.Thread(target=midnight, daemon=True) -tread_bots.start() -tread_midnight.start() diff --git a/longpulling.py b/longpulling.py new file mode 100644 index 0000000..3520080 --- /dev/null +++ b/longpulling.py @@ -0,0 +1,88 @@ +import datetime +import time +import logging +import dan63047VKbot +import config +import vk_api +import threading + +dan63047VKbot.log(False, "Script started") + +def bots(): + dan63047VKbot.log(False, "Started listening longpull server") + dan63047VKbot.debug_array['start_time'] = time.time() + for event in dan63047VKbot.MyVkLongPoll.listen(dan63047VKbot.longpoll): + try: + if event.type == dan63047VKbot.VkBotEventType.MESSAGE_NEW: + log_msg = f'[MESSAGE] id: {event.message.id}, peer_id: {event.message.peer_id}, user_id: {event.message.from_id}' + if event.message.action: + log_msg += f', action: '+event.message.action["type"]+', user id in action: '+ str(event.message.action["member_id"]) + if event.message.text != "": + log_msg += f', text: "{event.message.text}"' + if event.message.attachments: + atch = ', attachments: ' + for i in event.message.attachments: + if i['type'] == "sticker": + atch += f"sticker_id{i[i['type']]['sticker_id']}" + elif i['type'] == "wall": + atch += i['type'] + str(i[i['type']]['from_id']) + \ + "_" + str(i[i['type']]['id']) + " " + elif i['type'] == "link": + atch += i['type'] + " " + i[i['type']]['title'] + " " + else: + atch += i['type'] + str(i[i['type']]['owner_id']) + \ + "_" + str(i[i['type']]['id']) + " " + log_msg += atch + dan63047VKbot.log(False, log_msg) + dan63047VKbot.debug_array['messages_get'] += 1 + if int(event.message.peer_id) in dan63047VKbot.bot: + dan63047VKbot.bot[event.message.peer_id].get_message(event) + else: + dan63047VKbot.create_new_bot_object(event.message.peer_id) + dan63047VKbot.bot[event.message.peer_id].get_message(event) + elif event.type == dan63047VKbot.VkBotEventType.WALL_POST_NEW: + if event.object.post_type == "post": + dan63047VKbot.log(False, f"[NEW_POST] id{event.object.id}") + users = dan63047VKbot.db.get_all_users() + for i in users: + if (config.use_database): + dan63047VKbot.bot[int(i['chat_id'])].event("post", event.object) + else: + dan63047VKbot.bot[int(i)].event("post", event.object) + else: + dan63047VKbot.log(False, f"[NEW_OFFER] id{event.object.id}") + elif event.type == dan63047VKbot.VkBotEventType.MESSAGE_DENY: + dan63047VKbot.log(False, + f"User {event.object.user_id} deny messages from that group") + del dan63047VKbot.bot[int(event.object.user_id)] + dan63047VKbot.db.delete_user(event.object.user_id) + else: + dan63047VKbot.log(False, f"Event {str(event.type)} happend") + except Exception as kek: + dan63047VKbot.log(True, f"Беды с ботом: {str(kek)}") + dan63047VKbot.debug_array['bot_warnings'] += 1 + continue + + +def midnight(): + while True: + current_time = time.time()+10800 + if int(current_time) % 86400 == 0: + dan63047VKbot.log(False, "[EVENT_STARTED] \"Midnight\"") + users = dan63047VKbot.db.get_all_users() + for i in users: + if (config.use_database): + dan63047VKbot.bot[int(i['chat_id'])].event("midnight") + else: + dan63047VKbot.bot[int(i)].event("midnight") + dan63047VKbot.log(False, "[EVENT_ENDED] \"Midnight\"") + time.sleep(1) + else: + time.sleep(0.50) + + +dan63047VKbot.load_users() +tread_bots = threading.Thread(target=bots) +tread_midnight = threading.Thread(target=midnight, daemon=True) +tread_bots.start() +tread_midnight.start() \ No newline at end of file