diff --git a/README b/README index 5777ae6..95a74e5 100644 --- a/README +++ b/README @@ -9,10 +9,20 @@ Gaspar telegram bot written in python. Fetures: - * notify about topic update on rutracker via telegram. - * is able to push new magnet link into your Transmission RPC client. + * Notify about topic update on rutracker.org via Telegram. + * Can push new magnet link into your Transmission web client. running instance - @let_you_know_bot +⣿⣿⣿⡷⠊⡢⡹⣦⡑⢂⢕⢂⢕⢂⢕⢂⠕⠔⠌⠝⠛⠶⠶⢶⣦⣄⢂⢕⢂⢕ +⣿⣿⠏⣠⣾⣦⡐⢌⢿⣷⣦⣅⡑⠕⠡⠐⢿⠿⣛⠟⠛⠛⠛⠛⠡⢷⡈⢂⢕⢂ +⠟⣡⣾⣿⣿⣿⣿⣦⣑⠝⢿⣿⣿⣿⣿⣿⡵⢁⣤⣶⣶⣿⢿⢿⢿⡟⢻⣤⢑⢂ +⣾⣿⣿⡿⢟⣛⣻⣿⣿⣿⣦⣬⣙⣻⣿⣿⣷⣿⣿⢟⢝⢕⢕⢕⢕⢽⣿⣿⣷⣔ +⣿⣿⠵⠚⠉⢀⣀⣀⣈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣗⢕⢕⢕⢕⢕⢕⣽⣿⣿⣿⣿ +⢷⣂⣠⣴⣾⡿⡿⡻⡻⣿⣿⣴⣿⣿⣿⣿⣿⣿⣷⣵⣵⣵⣷⣿⣿⣿⣿⣿⣿⡿ +⢌⠻⣿⡿⡫⡪⡪⡪⡪⣺⣿⣿⣿⣿⣿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃ +⠣⡁⠹⡪⡪⡪⡪⣪⣾⣿⣿⣿⣿⠋⠐⢉⢍⢄⢌⠻⣿⣿⣿⣿⣿⣿⣿⣿⠏⠈ +⡣⡘⢄⠙⣾⣾⣾⣿⣿⣿⣿⣿⣿⡀⢐⢕⢕⢕⢕⢕⡘⣿⣿⣿⣿⣿⣿⠏⠠⠈ +⠌⢊⢂⢣⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢐⢕⢕⢕⢕⢕⢅⣿⣿⣿⣿⡿⢋⢜⠠⠈ diff --git a/gaspar/database.py b/gaspar/database.py index 195bbcf..8583aac 100644 --- a/gaspar/database.py +++ b/gaspar/database.py @@ -4,12 +4,15 @@ .. moduleauthor:: AB """ -import sqlite3 import logging import os +import sqlite3 + +from .transmission import check_connection log = logging.getLogger(__name__) + class DBInitException(Exception): """ Exception at DB Init """ @@ -19,6 +22,7 @@ class DBInitException(Exception): class DataBase: """This class create or use existent SQLite database file. It provides high-level methods for database.""" + def __init__(self): """ Constructor creates new SQLite database if @@ -85,7 +89,7 @@ class DataBase: :type conn: object :return: None """ - #log.debug("Close connection to %s", self.basefile) + # log.debug("Close connection to %s", self.basefile) conn.close() def copy_to_history(self, tor_id): @@ -106,13 +110,25 @@ class DataBase: self.execute(sql, attrs) def add_client(self, user_id, scheme, hostname, port, username, password, path): - sql = """INSERT OR REPLACE INTO tr_clients(user_id, scheme, hostname, port, username, password, path) - VALUES(?, ?, ?, ?, ?, ?, ?);""" - self.execute(sql, (user_id, scheme, hostname, port, username, password, path)) + if check_connection(scheme, hostname, port, username, password, path): + sql = """INSERT OR REPLACE INTO tr_clients(user_id, scheme, hostname, port, username, password, path) + VALUES(?, ?, ?, ?, ?, ?, ?);""" + self.execute(sql, (user_id, scheme, hostname, port, username, password, path)) + return True + else: + return False def get_client(self, user_id): sql = "SELECT scheme, hostname, port, username, password, path FROM tr_clients WHERE user_id = ?" - return self.execute(sql, (user_id,))[0] + res = self.execute(sql, (user_id,)) + if len(res): + return self.execute(sql, (user_id,))[0] + else: + return False + + def drop_client(self, user_id): + sql = "DELETE FROM tr_clients WHERE user_id = ?" + self.execute(sql, (user_id,)) def get_attr(self, tor_id, attr): sql = """SELECT %s FROM torrents WHERE id = ? ORDER BY reg_time DESC LIMIT 1""" % attr @@ -133,17 +149,17 @@ class DataBase: WHERE id = ? """ self.execute(sql, ( - tor_data["info_hash"], - tor_data["forum_id"], - tor_data["poster_id"], - int(tor_data["size"]), - int(tor_data["reg_time"]), - tor_data["tor_status"], - tor_data["seeders"], - tor_data["topic_title"], - tor_data["seeder_last_seen"], - tor_data["id"], - )) + tor_data["info_hash"], + tor_data["forum_id"], + tor_data["poster_id"], + int(tor_data["size"]), + int(tor_data["reg_time"]), + tor_data["tor_status"], + tor_data["seeders"], + tor_data["topic_title"], + tor_data["seeder_last_seen"], + tor_data["id"], + )) def save_tor(self, tor_data): sql = """INSERT OR IGNORE INTO torrents( @@ -159,17 +175,17 @@ class DataBase: 'seeder_last_seen' ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" self.execute(sql, ( - tor_data["id"], - tor_data["info_hash"], - tor_data["forum_id"], - tor_data["poster_id"], - int(tor_data["size"]), - int(tor_data["reg_time"]), - tor_data["tor_status"], - tor_data["seeders"], - tor_data["topic_title"], - tor_data["seeder_last_seen"], - )) + tor_data["id"], + tor_data["info_hash"], + tor_data["forum_id"], + tor_data["poster_id"], + int(tor_data["size"]), + int(tor_data["reg_time"]), + tor_data["tor_status"], + tor_data["seeders"], + tor_data["topic_title"], + tor_data["seeder_last_seen"], + )) def delete_tor(self, user_id, tor_id): sql = "DELETE FROM alerts WHERE user_id = ? AND tor_id = ?" @@ -183,20 +199,21 @@ class DataBase: 'last_name' ) VALUES (?, ?, ?, ?)""" self.execute(sql, ( - chat_instance['id'], - chat_instance['username'], - chat_instance['first_name'], - chat_instance['last_name'], - )) + chat_instance['id'], + chat_instance['username'], + chat_instance['first_name'], + chat_instance['last_name'], + )) + def save_alert(self, user_id, tor_id): sql = """INSERT OR IGNORE INTO alerts( 'user_id', 'tor_id' ) VALUES (?, ?)""" self.execute(sql, ( - user_id, - tor_id - )) + user_id, + tor_id + )) def get_alerts(self, user_id=None): if user_id: @@ -204,8 +221,8 @@ class DataBase: torrents t JOIN alerts a ON a.tor_id = t.id WHERE a.user_id = ?""" raw = self.execute(sql, ( - user_id, - )) + user_id, + )) else: sql = """SELECT t.size, t.reg_time, t.topic_title, t.id, t.info_hash FROM torrents t JOIN alerts a ON a.tor_id = t.id GROUP BY t.id""" @@ -227,6 +244,3 @@ class DataBase: for sub in self.execute(sql, (tor_id,)): subs.append(sub[0]) return subs - - - diff --git a/gaspar/gaspar.py b/gaspar/gaspar.py index 29afc98..3258332 100644 --- a/gaspar/gaspar.py +++ b/gaspar/gaspar.py @@ -1,12 +1,12 @@ +import logging import os import sys -import logging from urllib import parse -from telegram import * -from telegram.ext import Updater, MessageHandler, CommandHandler, PrefixHandler, filters -from .rutracker import Torrent + +from telegram.ext import Updater, MessageHandler, CommandHandler, filters + from .notify import update_watcher -from .database import DataBase +from .rutracker import Torrent from .tools import format_topic logging.basicConfig( @@ -14,14 +14,15 @@ logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') log = logging.getLogger(__name__) - token = os.environ.get('TG_TOKEN') if not token: log.error("Env var TG_TOKEN isn't set.") sys.exit(1) + def main(): """Run bot.""" + def add(update, context): if 'https://rutracker.org' in update.message.text: try: @@ -34,28 +35,27 @@ def main(): update.message.reply_text("Send me a URL to rutracker.org topic.") return log.info( - "Got /add request from user [%s] %s", - update.message.chat['id'], - update.message.from_user.username) + "Got /add request from user [%s] %s", + update.message.chat['id'], + update.message.from_user.username) torrent = Torrent(tor_id) torrent.db.save_tor(torrent.meta) torrent.db.save_user(update.message.chat) torrent.db.save_alert(update.message.chat['id'], torrent.meta['id']) msg = format_topic( - torrent.meta['id'], - torrent.meta['topic_title'], - torrent.meta['size'], - torrent.meta['info_hash'], - torrent.meta['reg_time'], - pre='You will be alerted about\n') + torrent.meta['id'], + torrent.meta['topic_title'], + torrent.meta['size'], + torrent.meta['info_hash'], + torrent.meta['reg_time'], + pre='You will be alerted about\n') update.message.reply_text(msg, parse_mode='HTML', disable_web_page_preview=True) - def list_alerts(update, context): log.info( - "Got /list request from user [%s] %s", - update.message.chat['id'], - update.message.from_user.username) + "Got /list request from user [%s] %s", + update.message.chat['id'], + update.message.from_user.username) alerts = Torrent().db.get_alerts(update.message.chat['id']) if len(alerts) == 0: update.message.reply_text("You have no configured alerts.") @@ -63,46 +63,65 @@ def main(): msg = "Configured alerts:\n" for alert in alerts: msg += format_topic( - alert['id'], - alert['topic_title'], - alert['size'], - alert['info_hash'], - alert['reg_time'], - pre="\n") + alert['id'], + alert['topic_title'], + alert['size'], + alert['info_hash'], + alert['reg_time'], + pre="\n") update.message.reply_text(msg, parse_mode='HTML', disable_web_page_preview=True) def handle_client(update, context): u_id = update.message.chat['id'] log.info( - "Got /client request from user [%s] %s", - u_id, - update.message.from_user.username) + "Got /client request from user [%s] %s", + u_id, + update.message.from_user.username) try: - addr = update.message.text.split()[1] - log.info("Client Transmission RPC address - %s", addr) - tr = parse.urlparse(addr) - scheme = tr.scheme if tr.scheme else False - hostname = tr.hostname if tr.hostname else False - username = tr.username if tr.username else False - password = tr.password if tr.password else False - path = tr.path if tr.path else '/transmission/rpc' - port = tr.port if tr.port else (80 if scheme == 'http' else 443) - if not scheme or not hostname: - update.message.reply_text( + addr = update.message.text.split()[1] + log.info("Client Transmission RPC address - %s", addr) + tr = parse.urlparse(addr) + scheme = tr.scheme if tr.scheme else False + hostname = tr.hostname if tr.hostname else False + username = tr.username if tr.username else False + password = tr.password if tr.password else False + path = tr.path if tr.path else '/transmission/rpc' + port = tr.port if tr.port else (80 if scheme == 'http' else 443) + if not scheme or not hostname: + update.message.reply_text( f'Can\'t understand : {update.message.text}. ' - 'Send transmission RPC address like http(s)://[user:pass]host[:port][/rpc_path]', + 'Send transmission RPC address like http(s)://[user:pass]host[:port][/transmission/rpc]', + parse_mode='HTML', + disable_web_page_preview=True) + return + except: + tr_client = Torrent().db.get_client(u_id) + log.info(tr_client) + if tr_client: + tr_line = f"Your client: {tr_client[0]}://{tr_client[1]}:{tr_client[2]}{tr_client[5]}\n" \ + r"/delete_client" + else: + tr_line = False + update.message.reply_text( + 'Gaspar can add new topics to your private Transmission server. ' + 'Send transmission RPC address like \nhttp(s)://[user:pass]host[:port][/transmission/rpc]\n' + f'{tr_line if tr_line else "You have no configured client."}', parse_mode='HTML', disable_web_page_preview=True) return - except: - update.message.reply_text( - 'Gaspar is able to add new topics to your private Transmission server.' - 'Send transmission RPC address like http(s)://[user:pass]host[:port][/rpc_path]', - parse_mode='HTML', - disable_web_page_preview=True) - return - Torrent().db.add_client(u_id, scheme, hostname, port, username, password, path) + if Torrent().db.add_client(u_id, scheme, hostname, port, username, password, path): + update.message.reply_text(f'Client reachable and saved.') + else: + update.message.reply_text(f'Client unreachable.') + + def delete_client(update, context): + log.info( + "Got /delete request from user [%s] %s", + update.message.chat['id'], + update.message.from_user.username) + Torrent().db.drop_client(update.message.chat['id']) + update.message.reply_text(f'Client deleted.') def delete(update, context): log.info( @@ -112,15 +131,16 @@ def main(): tor_id = update.message.text.split('_')[1] try: Torrent().db.delete_tor(update.message.chat['id'], tor_id) - context.bot.sendMessage(update.message.chat['id'], f'Deleted {tor_id}') + update.message.reply_text(f'Deleted {tor_id}') except: - context.bot.sendMessage(update.message.chat['id'], f'Faled to delete {tor_id}') + update.message.reply_text(f'Faled to delete {tor_id}') updater = Updater(token, use_context=True) update_watcher(updater.bot) updater.dispatcher.add_handler(CommandHandler('list', list_alerts)) updater.dispatcher.add_handler(CommandHandler('client', handle_client)) + updater.dispatcher.add_handler(CommandHandler('delete_client', delete_client)) updater.dispatcher.add_handler(MessageHandler(filters.Filters.regex(r'/delete_'), delete)) updater.dispatcher.add_handler(MessageHandler(filters.Filters.text, add)) diff --git a/gaspar/notify.py b/gaspar/notify.py index 5f318f4..311e514 100644 --- a/gaspar/notify.py +++ b/gaspar/notify.py @@ -1,25 +1,28 @@ -import time -import threading import logging +import threading +import time from datetime import datetime + from .rutracker import Torrent from .tools import format_topic from .transmission import add_tor -UPDATE_INTERVAL = 2 * 60 * 60 # in secs. +UPDATE_INTERVAL = 2 * 60 * 60 # in secs. log = logging.getLogger(__name__) torrent = Torrent() + def sizeof_fmt(num, suffix='B'): num = int(num) - for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: + for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: if abs(num) < 1024.0: return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 return "%.1f%s%s" % (num, 'Yi', suffix) + def update(tor_id): torrent.tor_id = tor_id if torrent.is_outdated(): @@ -29,6 +32,7 @@ def update(tor_id): else: return False + def update_watcher(bot): def __thread(): while True: @@ -36,35 +40,38 @@ def update_watcher(bot): raw = torrent.db.get_alerts() for alert in raw: alerts.append(alert['id']) - log.info("Checking for updates. Configured interval: %sh , [%s secs]", UPDATE_INTERVAL/60/60, UPDATE_INTERVAL) + log.info("Checking for updates. Configured interval: %sh , [%s secs]", UPDATE_INTERVAL / 60 / 60, + UPDATE_INTERVAL) log.info("Checking alert %s", alert['topic_title']) if update(alert['id']): log.info("Found update for [%s] %s", torrent.meta['id'], torrent.meta['topic_title']) reg_time = datetime.utcfromtimestamp(int(torrent.meta['reg_time']) - ).strftime('%b-%d-%Y') + ).strftime('%b-%d-%Y') msg = format_topic( - torrent.meta['id'], - torrent.meta['topic_title'], - torrent.meta['size'], - torrent.meta['info_hash'], - torrent.meta['reg_time'], - pre='Topic has been updated\n') + torrent.meta['id'], + torrent.meta['topic_title'], + torrent.meta['size'], + torrent.meta['info_hash'], + torrent.meta['reg_time'], + pre='Topic has been updated\n') subs = torrent.db.get_subscribers(alert['id']) for sub in subs: try: - scheme, hostname, port, username, password, path = torrent.db.get_client(sub) - if add_tor(scheme, hostname, port, username, password, path, torrent.meta['info_hash']): - log.info("Push update to client Transmission RPC for %s", torrent.meta['info_hash']) - msg = f"{msg}\n* Added to your Transmission: {scheme}://{hostname}:{port}/{path}" - else: - log.warning("Failed push update to client Transmission RPC for %s", torrent.meta['info_hash']) + scheme, hostname, port, username, password, path = torrent.db.get_client(sub) + if add_tor(scheme, hostname, port, username, password, path, torrent.meta['info_hash']): + log.info("Push update to client Transmission RPC for %s", torrent.meta['info_hash']) + msg = f"{msg}\n* Added to your Transmission: {scheme}://{hostname}:{port}/{path}" + else: + log.warning("Failed push update to client Transmission RPC for %s", + torrent.meta['info_hash']) except: - pass + pass bot.sendMessage(sub, msg, parse_mode='HTML', disable_web_page_preview=True) time.sleep(1) else: log.info("There is no update for %s", alert['topic_title']) time.sleep(UPDATE_INTERVAL) + update_thread = threading.Thread(target=__thread) update_thread.start() diff --git a/gaspar/rutracker.py b/gaspar/rutracker.py index 29e2262..2115476 100644 --- a/gaspar/rutracker.py +++ b/gaspar/rutracker.py @@ -1,5 +1,4 @@ import json -import os import logging import re import urllib.request @@ -8,6 +7,7 @@ from .database import DataBase log = logging.getLogger(__name__) + class Torrent: def __init__(self, tor_id=None): self.db = DataBase() @@ -27,7 +27,6 @@ class Torrent: self.__tor_id = tor_id if tor_id: return self.get_tor_topic_data(tor_id) - def get_tor_topic_data(self, tor_id): data = dict() @@ -62,6 +61,6 @@ class Torrent: if not self.tor_id: log.warn("Torrent id not presented.") return False - ep_str = re.search(r"\[\d+(\+\d+)?(-\d+)?( +)?(из)?( +)?\d+(\+\d+)?(-\d+)?\]", self.meta["topic_title"]).group(0) + ep_str = re.search(r"\[\d+(\+\d+)?(-\d+)?( +)?(из)?( +)?\d+(\+\d+)?(-\d+)?\]", self.meta["topic_title"]).group( + 0) return ep_str - diff --git a/gaspar/tools.py b/gaspar/tools.py index 222608a..f5cb648 100644 --- a/gaspar/tools.py +++ b/gaspar/tools.py @@ -1,16 +1,18 @@ from datetime import datetime + def format_topic(tor_id, topic_title, size, info_hash, reg_time, pre=''): def sizeof_fmt(num, suffix='B'): num = int(num) - for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: + for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: if abs(num) < 1024.0: return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 return "%.1f%s%s" % (num, 'Yi', suffix) - size = sizeof_fmt(size) + + size = sizeof_fmt(size) reg_time = datetime.utcfromtimestamp(int(reg_time) - ).strftime('%b-%d-%Y') + ).strftime('%b-%d-%Y') msg = f"""{pre}{topic_title} 💿 Size: {size} #️⃣ Hash: {info_hash} diff --git a/gaspar/transmission.py b/gaspar/transmission.py index 05636cf..14b3d05 100644 --- a/gaspar/transmission.py +++ b/gaspar/transmission.py @@ -1,17 +1,31 @@ from transmission_rpc import Client -def add_tor(scheme, hostname, port, username, password, path, tor_hash): - try: - c = Client( - host=hostname, - port=port, - username=username, - password=password, - protocol=scheme, - path=path) - m = f'magnet:?xt=urn:btih:{tor_hash}' - c.add_torrent(m) - return True - except: - return False +def add_tor(scheme, hostname, port, username, password, path, tor_hash): + try: + c = Client( + host=hostname, + port=port, + username=username, + password=password, + protocol=scheme, + path=path) + m = f'magnet:?xt=urn:btih:{tor_hash}' + c.add_torrent(m) + return True + except: + return False + + +def check_connection(scheme, hostname, port, username, password, path): + try: + c = Client( + host=hostname, + port=port, + username=username, + password=password, + protocol=scheme, + path=path) + return True if c.rpc_version else False + except: + return False