diff --git a/PageSaver/package.json b/PageSaver/package.json new file mode 100644 index 0000000..bbc651f --- /dev/null +++ b/PageSaver/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "puppeteer": "^14.4.0" + } +} diff --git a/PageSaver/pageSaver b/PageSaver/pageSaver new file mode 100644 index 0000000..856f458 Binary files /dev/null and b/PageSaver/pageSaver differ diff --git a/PageSaver/pageSaver.js b/PageSaver/pageSaver.js new file mode 100644 index 0000000..1b241e5 --- /dev/null +++ b/PageSaver/pageSaver.js @@ -0,0 +1,18 @@ +// npm install https://github.com/GoogleChrome/puppeteer/ + +const puppeteer = require('puppeteer'); + +(async () => { + + const url = process.argv[2]; + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + await page.goto(url, {waitUntil: 'load'}); + + const html = await page.content(); + + browser.close(); + console.log(html); + +})(); diff --git a/bwtbot.py b/bwtbot.py new file mode 100644 index 0000000..47b0ae7 --- /dev/null +++ b/bwtbot.py @@ -0,0 +1,173 @@ +#-*- coding: utf-8 -*- + +import traceback +from pyrogram import Client, filters, idle +from pyrogram.types import ForceReply, BotCommand, BotCommandScopeChat +from pyrogram.enums.chat_action import ChatAction +from functions import * +from modules.colors import * +from modules.bwt import * +import subprocess +import os + +config = jsonLoad("config.json") + +owner_id = config["owner_id"] + +app = Client(config["bot_name"], api_id=config["api_id"], api_hash=config["api_hash"], bot_token=config["bot_token"]) + + +@app.on_message(~ filters.scheduled & filters.command(["setcard", "задать карту"], prefixes=["/", ""])) +async def setcard(_, msg): + if userGet(msg.from_user.id, "context") is None: + userSet(msg.from_user.id, "context", "set") + await msg.reply_text(string("send_number"), reply_markup=ForceReply(placeholder=string("enter_number"))) + else: + await msg.reply_text(string("cancel_first")) + + +@app.on_message(~ filters.scheduled & filters.command(["cancel", "відміна"], prefixes=["/", ""])) +async def cancel(_, msg): + if userGet(msg.from_user.id, "context") is not None: + userReset(msg.from_user.id, "context") + await msg.reply_text(string("cancel")) + else: + await msg.reply_text(string("cancel_none")) + + +@app.on_message(~ filters.scheduled & filters.command(["resetcard", "забути картку"], prefixes=["/", ""])) +async def resetcard(_, msg): + if userGet(msg.from_user.id, "context") is None: + if "card" in jsonLoad("data/database.json")[str(msg.from_user.id)]: + userReset(msg.from_user.id, "card") + await msg.reply_text(string("card_unlinked")) + appendLog(f"User {str(msg.from_user.id)} reseted his card") + else: + await msg.reply_text(string("card_not_linked").format(string("get_number"))) + appendLog(f"User {str(msg.from_user.id)} tried to reset non-existent card") + else: + await msg.reply_text(string("cancel_first")) + + +@app.on_message(~ filters.scheduled & filters.command(["balance", "баланс"], prefixes=["/", ""])) +async def balance(_, msg): + if userGet(msg.from_user.id, "context") is None: + try: + if "card" in jsonLoad("data/database.json")[str(msg.from_user.id)]: + await app.send_chat_action(chat_id=msg.chat.id, action=ChatAction.TYPING) + water_left = await getWaterLeft(userGet(msg.from_user.id, "card"), msg.from_user.id, app) + if water_left == "": + raise EmptyCardException("Card information is empty") + elif water_left == "Failure": + await msg.reply_text(string("error_occured").format(string("get_number"))) + appendLog(f"User {str(msg.from_user.id)} could not get left water amount") + else: + await msg.reply_text(string("card_balance").format(water_left)) + appendLog(f"User {str(msg.from_user.id)} has {water_left} liters remaining") + else: + await msg.reply_text(string("card_not_linked").format(string("get_number"))) + appendLog(f"User {str(msg.from_user.id)} tried to get balance without card set") + except Exception as exp: + await msg.reply_text(string("error_occured").format(string("get_number"))) + await app.send_message(owner_id, f"Error occured by {str(msg.from_user.id)}:\nException: `{exp}`\nTraceback: `{traceback.format_exc()}`") + appendLog(f"User {str(msg.from_user.id)} could not get left water amount") + else: + await msg.reply_text(string("cancel_first")) + + +@app.on_message(~ filters.scheduled & filters.command(["topup", "refill", "поповнити"], prefixes=["/", ""])) +async def topup_cmd(_, msg): + if userGet(msg.from_user.id, "context") is None: + try: + if "card" in jsonLoad("data/database.json")[str(msg.from_user.id)]: + await app.send_chat_action(chat_id=msg.chat.id, action=ChatAction.TYPING) + await msg.reply_text(string("top_up").format(str(userGet(msg.from_user.id, "card")))) + appendLog(f"User {str(msg.from_user.id)} requested top up") + else: + await msg.reply_text(string("card_not_linked").format(string("get_number"))) + appendLog(f"User {str(msg.from_user.id)} tried to request top up without card set") + except Exception as exp: + await msg.reply_text(exp) + else: + await msg.reply_text(string("cancel_first")) + + +@app.on_message(~ filters.scheduled & filters.command(["start", "help", "допомога"], prefixes=["/", ""])) +async def help(_, msg): + if userGet(msg.from_user.id, "context") is None: + await msg.reply_text(string("welcome").format(string("get_number"))) + if msg.from_user.language_code in jsonLoad("strings.json"): + userSet(msg.from_user.id, "locale", msg.from_user.language_code) + else: + userSet(msg.from_user.id, "locale", "en") + else: + await msg.reply_text(string("cancel_first")) + +pid = os.getpid() + +@app.on_message(~ filters.scheduled & filters.command(["kill", "die", "shutdown"], prefixes="/")) +async def kill(_, msg): + if msg.from_user.id == owner_id: + await msg.reply_text(f"Shutting down bot with pid **{pid}**") + os.system(f"kill -9 {pid}") + + +@app.on_message(~ filters.scheduled) +async def any_message_handler(app, msg): + if userGet(msg.from_user.id, "context") == "set": + userSet(msg.from_user.id, "card", msg.text) + userReset(msg.from_user.id, "context") + appendLog(f"User {str(msg.from_user.id)} set card id to {msg.text}") + await msg.reply_text(string("card_linked").format(msg.text)) + +print(f'{nowtime()} {WHITE}Starting with PID {YELLOW}{pid}{RESET}') + +app.start() +app.send_message(owner_id, f"Starting bot with pid **{pid}**") + +app.set_bot_commands([ + BotCommand("help", "Меню допомоги"), + BotCommand("balance", "Баланс картки"), + BotCommand("topup", "Поповнити картку"), + BotCommand("setcard", "Прив'язати картку"), + BotCommand("resetcard", "Відв'язати картку"), + BotCommand("cancel", "Відмінити операцію"), + ], + language_code="uk") + +app.set_bot_commands([ + BotCommand("help", "Меню допомоги"), + BotCommand("balance", "Баланс картки"), + BotCommand("topup", "Поповнити картку"), + BotCommand("setcard", "Прив'язати картку"), + BotCommand("resetcard", "Відв'язати картку"), + BotCommand("cancel", "Відмінити операцію"), + ], + language_code="ru") + +app.set_bot_commands([ + BotCommand("help", "Help menu"), + BotCommand("balance", "Card's balance"), + BotCommand("topup", "Refill card"), + BotCommand("setcard", "Link card"), + BotCommand("resetcard", "Unlink card"), + BotCommand("cancel", "Cancel operation"), + ]) + +app.set_bot_commands([ + BotCommand("help", "Help menu"), + BotCommand("balance", "Card's balance"), + BotCommand("topup", "Refill card"), + BotCommand("setcard", "Link card"), + BotCommand("resetcard", "Unlink card"), + BotCommand("shutdown", "Turn off the bot"), + BotCommand("cancel", "Cancel operation"), + ], + scope=BotCommandScopeChat(chat_id=owner_id)) + +idle() + +app.send_message(owner_id, f"Shutting down bot with pid **{pid}**") +print(f'\n{nowtime()} {WHITE}Shutting down with PID {YELLOW}{pid}{RESET}') + +subprocess.call(f'kill -9 {pid}', shell=True) diff --git a/config.json b/config.json new file mode 100644 index 0000000..6fc5ab5 --- /dev/null +++ b/config.json @@ -0,0 +1,8 @@ +{ + "owner_id": 0, + "log_size": 1024, + "api_id": 0, + "api_hash": "", + "bot_token": "", + "bot_name": "" +} \ No newline at end of file diff --git a/data/database.json b/data/database.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/data/database.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/data/pages/.gitkeep b/data/pages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/functions.py b/functions.py new file mode 100644 index 0000000..87853b2 --- /dev/null +++ b/functions.py @@ -0,0 +1,121 @@ +import json +import os +import shutil +import gzip +import time +from modules.colors import * +from datetime import datetime +from pathlib import Path + +path = Path(__file__).resolve().parent + +days_path = str(path)+"/assets/days/" +users_path = str(path)+"/users/" +logs_folder = str(path)+"/logs/" + + +def jsonSave(filename, value): + with open(filename, 'w', encoding="utf-8") as f: + json.dump(value, f, indent=4, ensure_ascii=False) + f.close() + +def jsonLoad(filename): + with open(filename, 'r', encoding="utf-8") as f: + value = json.load(f) + f.close() + return value + + +config = jsonLoad(f"{path}/config.json") + +log_size = config["log_size"] +owner_id = config["owner_id"] + + +def nowtime(): + return f'{BBLACK}[{CYAN}{datetime.now().strftime("%H:%M:%S")}{BBLACK}]{RESET}' + +def checkSize(): + global logs_folder, log_size + + 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 appendLog(message): + global logs_folder + + checkSize() + + 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: + time.sleep(2) + print('Log file could not be created') + return + + log.write(f'[{datetime.now().strftime("%H:%M:%S | %d.%m.%Y")}] {message}\n') + + log.close() + + +def string(key: str, *args: str, userlocale="uk"): + locales = jsonLoad("strings.json") + strings = locales[userlocale] + string = strings + for dict_key in args: + string = string[dict_key] + return string[key] + + +def userSet(userid, key: str, value): + database = jsonLoad("data/database.json") + if str(userid) not in database: + database[str(userid)] = {} + database[str(userid)][key] = value + jsonSave("data/database.json", database) + +def userReset(userid, key: str): + database = jsonLoad("data/database.json") + del database[str(userid)][key] + jsonSave("data/database.json", database) + +def userGet(userid, key: str): + try: + return jsonLoad("data/database.json")[str(userid)][key] + except KeyError: + return None + except FileNotFoundError: + return None \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/modules/bwt.py b/modules/bwt.py new file mode 100644 index 0000000..9d89b7a --- /dev/null +++ b/modules/bwt.py @@ -0,0 +1,50 @@ +#-*- coding: utf-8 -*- + +import os +import traceback +from functions import * +from bs4 import BeautifulSoup + +config = jsonLoad("config.json") + +class EmptyCardException(Exception): + pass + +async def getWaterLeft(cardid, filename, app=None): + + url = f"https://bwtaqua.com.ua/card-topup/?id={cardid}" + + try: + + os.system(f'touch data/pages/{str(filename)}.html') + os.system(f'PageSaver/pageSaver "https://bwtaqua.com.ua/card-topup/?id={cardid}" > data/pages/{str(filename)}.html') + + with open(f'data/pages/{str(filename)}.html') as f: + html_file = f.read() + f.close() + + soup = BeautifulSoup(html_file, 'html.parser') + + output = (soup.find_all("h3", class_="headline headline_center headline_pink js-payment-balance")[0].getText()).replace("Твій баланс ", "").replace(" л", "") + + appendLog(f"Parsed {output} liters of water remaining (user: {str(filename)}, cardid: {cardid})") + + except Exception as exp: + + appendLog(f"Exception occured: {exp} (user: {str(filename)}, cardid: {cardid})") + + if app != None: + await app.send_message(config["owner_id"], f"**Exception occured:**\n • User: `{str(filename)}`\n • Card: [{cardid}]({url})\n • Exception: `{exp}`\n • Traceback: `{traceback.format_exc()}`", disable_web_page_preview=True) + else: + print(f'Exception occured and could not send to user: {exp}') + + output = "Failure" + + return output + +if __name__ == "__main__": + + cardid = input("Enter card number: ") + userid = input("Enter Telegram ID (optional): ") + + print(f"Card has {str(getWaterLeft(cardid, userid, app=None))} l. left") diff --git a/modules/colors.py b/modules/colors.py new file mode 100644 index 0000000..3a623b1 --- /dev/null +++ b/modules/colors.py @@ -0,0 +1,22 @@ +RESET = '\u001b[0m' + +BLACK = '\u001b[30m' +RED = '\u001b[31m' +GREEN = '\u001b[32m' +YELLOW = '\u001b[33m' +BLUE = '\u001b[34m' +MAGENTA = '\u001b[35m' +CYAN = '\u001b[36m' +WHITE = '\u001b[37m' + +BBLACK = '\u001b[30;1m' +BRED = '\u001b[31;1m' +BGREEN = '\u001b[32;1m' +BYELLOW = '\u001b[33;1m' +BBLUE = '\u001b[34;1m' +BMAGENTA = '\u001b[35;1m' +BCYAN = '\u001b[36;1m' +BWHITE = '\u001b[37;1m' + +ULINE = '\u001b[4m' +REVERSE = '\u001b[7m' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..041f722 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +beautifulsoup4 \ No newline at end of file diff --git a/strings.json b/strings.json new file mode 100644 index 0000000..e0ad20b --- /dev/null +++ b/strings.json @@ -0,0 +1,32 @@ +{ + "en": { + "welcome": "Welcome!\n\nThis bot allows you to get liters left on your personal BWT card.\n\n**Commands:**\n • /balance – get card balance\n • /setcard – link your card\n • /resetcard – unlink your card\n\n{0}\n\nDeveloper **is not affiliated with BWT Aqua** and this bot is made for personal usage only.", + "get_number": "**Get card number (Var. 1):**\nOn the front bottom side of your card, number may be found\n\n**Get card number (Var. 2):**\n1. Scan QR on the card\n2. Open webpage from code\n3. Numer should be found in **Номер карти \"Здорова Вода\"** or **Номер карти BWT Aqua** fields", + "card_linked": "Linked card: `{0}`\n\nPlease, make sure the number is correct before using the bot", + "card_unlinked": "Card was unlinked from your Telegram", + "card_not_linked": "You don't have any linked card.\n\nВы можете задать её с помощью команды /setcard\n\n{0}", + "error_occured": "An error occurred while getting the amount of remaining water on the card.\n\nPlease make sure the linked card number is correct. If you are sure that the bot is broken, please contact @profitroll.\n\nLink your card: /setcard\n\n{0}", + "card_balance": "Card's balance is {0} l. of water", + "top_up": "[Click here to top up](https://bwtaqua.com.ua/card-topup/?id={0})", + "cancel": "Operation cancelled", + "cancel_none": "Nothing to cancel", + "cancel_first": "Operation ongoing. Cancel the current one using /cancel to run this action", + "enter_number": "Enter card number", + "send_number": "Please, send your card number" + }, + "uk": { + "welcome": "Привіт-привіт!\n\nЦей бот дозволяє дізнатись скільки літрів залишилось на вашій карточці.\n\n**Команди:**\n • /balance – дізнатись баланс карти\n • /setcard – приав'язати карту\n • /resetcard – відв'язати карту\n\n{0}\n\nРозробник **не має жодного відношення до BWT Aqua**, а бот створений лише для особистого, некомерційного використання.", + "get_number": "**Дізнатись номер картки (Вар. 1):**\nЗ лицевої сторони картки знизу може бут вказано номер цієї картки\n\n**Дізнатись номер картки (Вар. 2):**\n1. Отсканувати QR код на картці\n2. Відкрити веб-сторінку з кода\n3. Номер буде знаходитись в полі **Номер карти \"Здорова Вода\"** або **Номер карти BWT Aqua**", + "card_linked": "Прив'язана карточка: `{0}`\n\nБудь ласка, упевніться що номер правильний перед використанням інших команд", + "card_unlinked": "Картку відв'язано від вашого Telegram", + "card_not_linked": "У вас немає прив'язаної картки.\n\nВи можете зробити це за допомогою команди /setcard\n\n{0}", + "error_occured": "При отриманні води на карточці виникла помилка.\n\nБудь ласка, упевніться що номер карти правильний. Якщо ви впевнені, що номер картки правильний та бот зламався – зв'яжіться з @profitroll.\n\nПрив'язати карту: /setcard\n\n{0}", + "card_balance": "На карточці {0} л. води", + "top_up": "[Натисніть для поповнення](https://bwtaqua.com.ua/card-topup/?id={0})", + "cancel": "Операція відмінена", + "cancel_none": "Нема що відміняти", + "cancel_first": "Триває інша операція. Відмініть триваючу операцію командою /cancel щоб запустити іншу дію", + "enter_number": "Введіть номер картки", + "send_number": "Будь ласка, надішліть номер вашої картки" + } +} \ No newline at end of file