diff --git a/.gitignore b/.gitignore index 55be276..457bd40 100644 --- a/.gitignore +++ b/.gitignore @@ -152,3 +152,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +loop.sh \ No newline at end of file diff --git a/README.md b/README.md index a839c92..a3b6aeb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # AutoZoomTelegram -Telegram bot used to manage AutoZoom meetings \ No newline at end of file +Telegram bot used to manage AutoZoom meetings + +Install all the requirements from `requirements.txt`, configure your `config.json` and let it run! \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..ad9d16c --- /dev/null +++ b/bot.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +import os +import subprocess + +from sys import exit +from modules.functions import * +from modules.functions_bot import * +from pyrogram import Client, filters, idle +from pyrogram.types import ForceReply, BotCommand, BotCommandScopeChat +from pyrogram.enums.chat_action import ChatAction + +if configGet("bot_token") != "12345678:asdfghjklzxcvbnm": + pid = os.getpid() + app = Client("auto_zoom_public_bot", api_id=configGet("api_id"), api_hash=configGet("api_hash"), bot_token=configGet("bot_token")) +else: + logWrite("Could not start the bot. Please, configure token in config.json") + exit() + +def botSend(userid, message="Test message"): + app = Client("auto_zoom_public_bot") + app.send_message(userid, message) + + +@app.on_message(~ filters.scheduled & filters.command(["link", "start"], prefixes="/")) +def start(app, msg): + + logWrite(f'Got command start/link from {msg.from_user.id}') + + app.send_chat_action(chat_id=msg.chat.id, action=ChatAction.TYPING) + + if f"{msg.from_user.id}.json" not in os.listdir("data/users/"): + logWrite(f'Creating blank data file for {msg.from_user.id}') + jsonSave( f"data/users/{msg.from_user.id}.json", {"api_key": None, "linked": False, "context": {"action": None, "data": None}} ) + + if not userGet(msg.from_user.id, "linked"): + msg.reply_text(locale("link_input", "msg"), reply_markup=ForceReply(placeholder=locale("link", "fry"))) + userSet(msg.chat.id, "context", "link_key") + else: + msg.reply_text(locale("already_linked", "msg")) + +@app.on_message(~ filters.scheduled & filters.command(["unlink"], prefixes="/")) +def unlink(app, msg): + + logWrite(f'Got command ulink from {msg.from_user.id}') + + app.send_chat_action(chat_id=msg.chat.id, action=ChatAction.TYPING) + + if not userGet(msg.from_user.id, "linked"): + msg.reply_text(locale("not_linked", "msg")) + else: + try: + keys_storage = jsonLoad("data/keys_storage.json") + del keys_storage[userGet(msg.from_user.id, "api_key")] + jsonSave("data/keys_storage.json", keys_storage) + except: + pass + userClear(msg.from_user.id, "api_key") + userSet(msg.chat.id, "linked", False) + msg.reply_text(locale("unlinked", "msg")) + +@app.on_message(~ filters.scheduled & filters.command(["cancel"], prefixes="/")) +def cancel(app, msg): + + app.send_chat_action(chat_id=msg.chat.id, action=ChatAction.TYPING) + + if userGet(msg.from_user.id, "context") is not None: + userClear(msg.from_user.id, "context") + userClear(msg.from_user.id, "context_content") + msg.reply_text(locale("cancel", "msg")) + else: + msg.reply_text(locale("cancel_empty", "msg")) + + +@app.on_message(filters.command(["kill", "die", "shutdown", "reboot"], prefixes="/")) +def kill(app, msg): + + if msg.from_user.id == configGet("admin"): + msg.reply_text(f"Shutting down bot with pid `{pid}`") + os.system('kill -9 '+str(pid)) + + +@app.on_message(~ filters.scheduled) +def any_message_handler(app, msg): + + if userGet(msg.from_user.id, "context") == "link_key": + + if msg.text in jsonLoad(configGet("api_keys"))["autozoom"]: + msg.reply_text(locale("key_correct", "msg")) + userSet(msg.from_user.id, "api_key", msg.text) + userSet(msg.from_user.id, "linked", True) + keys_storage = jsonLoad("data/keys_storage.json") + keys_storage[msg.text] = msg.from_user.id + jsonSave("data/keys_storage.json", keys_storage) + logWrite(f"Added apikey {msg.text} for user {msg.from_user.id}") + else: + logWrite(f"User {msg.from_user.id} tried to pair with invalid apikey {msg.text}") + msg.reply_text(locale("key_wrong", "msg")) + + userClear(msg.from_user.id, "context") + userClear(msg.from_user.id, "context_content") + + +if __name__ == "__main__": + + logWrite(f'Starting with PID {str(pid)}') + + app.start() + app.send_message(configGet("admin"), f"Starting bot with pid `{pid}`") + + all_commands = locale("cmd") + + commands_list = [] + for command in all_commands["general"]: + commands_list.append(BotCommand(command, all_commands["general"][command])) + app.set_bot_commands(commands_list) + + # Registering admin commands + commands_admin_list = [] + for command in all_commands["general"]: + commands_admin_list.append(BotCommand(command, all_commands["general"][command])) + for command in all_commands["admin"]: + commands_admin_list.append(BotCommand(command, all_commands["admin"][command])) + app.set_bot_commands(commands_admin_list, scope=BotCommandScopeChat(chat_id=configGet("admin"))) + + idle() + + app.send_message(configGet("admin"), f"Shutting down bot with pid `{pid}`") + logWrite(f'Shutting down with PID {pid}') + + subprocess.call(f'kill -9 {pid}', shell=True) \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..09bc830 --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "admin": 123456789, + "api_id": 1234567, + "api_hash": "01234567890qwertyuiop", + "bot_token": "12345678:asdfghjklzxcvbnm", + "api_keys": "data/api_keys.json" +} \ No newline at end of file diff --git a/data/keys_storage.json b/data/keys_storage.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/data/keys_storage.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/data/users/.gitkeep b/data/users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/modules/functions.py b/modules/functions.py new file mode 100644 index 0000000..733c268 --- /dev/null +++ b/modules/functions.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""Some set of functions needed for discord/telegram bots and other types of apps""" + +import gzip +import os +import shutil + +from datetime import datetime +from ujson import loads, dumps + + +def nowtimeGet(format="%H:%M:%S | %d.%m.%Y"): + """Return current local time formatted as arg. + + ### Args: + * format (str, optional): Format that should be returned. Defaults to "%H:%M:%S | %d.%m.%Y". + + ### Returns: + * str: Local time formatted as arg. + """ + return datetime.now().strftime(format) + + +def checkSize(logs_folder="logs/", log_size=1024): + """Checks latest log file size and rotates it if needed. + + ### Args: + * logs_folder (str, optional): Folder where logs stored. Defaults to "logs/". + * log_size (int, optional): How many bytes should file containt to be rotated. Defaults to 1024. + """ + i = 0 + while i < 2: + try: + log = os.stat(logs_folder + 'latest.log') + if (log.st_size / 1024) > log_size: + with open(logs_folder + 'latest.log', 'rb') as f_in: + with gzip.open(f'{logs_folder}{datetime.now().strftime("%d.%m.%Y_%H:%M:%S")}.zip', 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + open(logs_folder + 'latest.log', 'w').close() + i = 2 + except FileNotFoundError: + try: + log = open(logs_folder + 'latest.log', 'a') + open(logs_folder + 'latest.log', 'a').close() + except: + try: + os.mkdir(logs_folder) + log = open(logs_folder + 'latest.log', 'a') + open(logs_folder + 'latest.log', 'a').close() + except: + pass + i += 1 + + +def logWrite(message, logs_folder="logs/", level="INFO"): + """Append some message to latest log file. + + ### Args: + * message (str): Something you want to add to log + * logs_folder (str, optional): Folder where logs stored. Defaults to "logs/". + * level (str, optional): Log level (INFO, WARN, ERRO, CRIT, DEBG). Defaults to "INFO". + """ + + checkSize(logs_folder=logs_folder) + + try: + log = open(logs_folder + 'latest.log', 'a') + open(logs_folder + 'latest.log', 'a').close() + except: + try: + os.mkdir(logs_folder) + log = open(logs_folder + 'latest.log', 'a') + open(logs_folder + 'latest.log', 'a').close() + except: + print(f'[{nowtimeGet()}] [ERRO] Log file could not be written.', flush=True) + return + + log.write(f'[{nowtimeGet()}] [{level}] {message}\n') + print(f'[{nowtimeGet()}] [{level}] {message}', flush=True) + log.close() + + +def jsonSave(filename, value): + """Save some list or dict as json file. + + Args: + * filename (str): File to which value will be written. + * value (list or dict): Some object that will be written to filename. + """ + with open(filename, 'w', encoding="utf-8") as f: + f.write(dumps(value, indent=4, ensure_ascii=False)) + f.close() + + +def jsonLoad(filename): + """Load json file and return python dict or list. + + Args: + * filename (str): File which should be loaded. + + Returns: + * list or dict: Content of json file provided. + """ + with open(filename, 'r', encoding="utf-8") as f: + value = loads(f.read()) + f.close() + return value \ No newline at end of file diff --git a/modules/functions_bot.py b/modules/functions_bot.py new file mode 100644 index 0000000..ded028e --- /dev/null +++ b/modules/functions_bot.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from modules.functions import jsonLoad, jsonSave + +def locale(key, *args): + strings = jsonLoad("strings.json") + string = strings + for dict_key in args: + string = string[dict_key] + return string[key] + + +def configGet(key): + return jsonLoad("config.json")[key] + +def configAppend(key, value): + config = jsonLoad("config.json") + config[key].append(value) + jsonSave("config.json", config) + +def configRemove(key, value): + config = jsonLoad("config.json") + config[key].remove(value) + jsonSave("config.json", config) + + +def userSet(userid, key, value): + user = jsonLoad(f"data/users/{userid}.json") + user[key] = value + jsonSave(f"data/users/{userid}.json", user) + +def userGet(userid, key): + try: + return jsonLoad(f"data/users/{userid}.json")[key] + except KeyError: + return None + except FileNotFoundError: + return None + +def userClear(userid, key): + try: + user = jsonLoad(f"data/users/{userid}.json") + del user[key] + jsonSave(f"data/users/{userid}.json", user) + except KeyError: + pass \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d5da089 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Pyrogram>=2.0.0 +ujson \ No newline at end of file diff --git a/strings.json b/strings.json new file mode 100644 index 0000000..6ae877d --- /dev/null +++ b/strings.json @@ -0,0 +1,28 @@ +{ + "btn": {}, + "msg": { + "link_input": "Please, send your personal key got from AutoZoom application.\n\n**Need any help?**\nLearn how linking works on [our website](https://www.end-play.xyz/autozoom/bot) or contact [our support](https://support.end-play.xyz) if guide page didn't help you.", + "cancel": "Operation cancelled.", + "cancel_empty": "Nothing to cancel.", + "key_correct": "✅ **Account successfully linked**\nYou will now receive all meetings notifications. You can also add new meetings to your AutoZoom app using /meeting command.", + "key_wrong": "❌ **Personal key is incorrect**\nGet your linking key using your AutoZoom application and try again by using bot's /link command.\n\n**Need any help?**\nLearn how linking works on [our website](https://www.end-play.xyz/autozoom/bot) or contact [our support](https://support.end-play.xyz) if guide page didn't help you.", + "already_linked": "❌ **Account is already linked**\nIf you want to change your personal key, then you need to /unlink your account first and try to /link it once more.", + "not_linked": "❌ **Account is not linked**\nYou need to /link your account to AutoZoom application first.", + "unlinked": "✅ **Account successfully unlinked**\nYou can now /link it to another AutoZoom application if you want." + }, + "clb": {}, + "fry": { + "link": "Key received in your AutoZoom app" + }, + "cmd": { + "general": { + "link": "Connect to your AutoZoom", + "unlink": "Disconnect from your AutoZoom", + "meeting": "Add new Zoom meeting", + "cancel": "Cancel any current operation" + }, + "admin": { + "reboot": "Shut the bot down" + } + } +} \ No newline at end of file