diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index a2215dc..f26d46c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,8 @@ services: container_name: wallamanta-bot restart: unless-stopped environment: - - TELEGRAM_CHANNEL_ID=${TELEGRAM_CHANNEL_ID} - - TELEGRAM_TOKEN=${TELEGRAM_TOKEN} - - LATITUDE=${LATITUDE} - - LONGITUDE=${LONGITUDE} - - SLEEP_TIME=${SLEEP_TIME} + - TELEGRAM_CHANNEL_ID=${TELEGRAM_CHANNEL_ID} + - TELEGRAM_TOKEN=${TELEGRAM_TOKEN} + - LATITUDE=${LATITUDE} + - LONGITUDE=${LONGITUDE} + - SLEEP_TIME=${SLEEP_TIME} diff --git a/wallamanta/__init__.py b/wallamanta/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/wallamanta/alert.py b/wallamanta/alert.py index 237807b..8630d1e 100644 --- a/wallamanta/alert.py +++ b/wallamanta/alert.py @@ -2,6 +2,7 @@ import json import os import threading import logging +import prettytable from worker import Worker from telegram import ForceReply, Update @@ -21,9 +22,13 @@ logging.basicConfig( logger = logging.getLogger(__name__) def parse_json_file(): - f = open("args.json") + f = open("products.json") return json.load(f) +def save_json_file(products): + with open('products.json', 'w') as outfile: + json.dump(products, outfile, indent=2) + async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Send a message when the command /help is issued.""" message = "Add with /add product;min_price;max_price" @@ -32,10 +37,11 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No async def add_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: parsed = update.message.text.split(";") logging.info(parsed) - if len(parsed) != 3: - message = "You must put the correct number of arguments: /add product;min_price;max_price" + if len(parsed) < 3: + #message = "You must pass the correct number of arguments: /add product;min_price;max_price;latitude(optional);longitude(optional);distance(optional);title_exclude(optional);title_description_exclude(optional)" + message = "You must pass the correct number of arguments: /add product;min_price;max_price" else: - argument = {"product_name": f"{parsed[0][5:]}", #removes "/add " + product = {"product_name": f"{parsed[0][len('/add '):]}", #removes "/add " "distance": "0", "latitude": f"{LATITUDE}", "longitude": f"{LONGITUDE}", @@ -45,31 +51,40 @@ async def add_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non "title_keyword_exclude" : [], "exclude": [] } - p = threading.Thread(target=Worker.run, args=(argument, )) + products = parse_json_file() + products.append(product) + save_json_file(products) + p = threading.Thread(target=Worker.run, args=(product, )) p.start() message = f"Added {parsed[0][5:]}" await update.message.reply_text(message) async def remove_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - await update.message.reply_text("Help!") + products = parse_json_file() + product_to_remove = update.message.text[len('/remove '):] + for product in products: + if product['product_name'] == product_to_remove: + products.remove(product) + save_json_file(products) + await update.message.reply_text(f"{product_to_remove} removed!") async def list_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - args = parse_json_file() - product_list = "" - for product in args: - product_list = f"{product_list}\n{product['product_name']};{product['min_price']}-{product['max_price']}" - await update.message.reply_text(product_list) + products = parse_json_file() -async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Echo the user message.""" - await update.message.reply_text(update.message.text) + table = prettytable.PrettyTable(['Product', 'Min', 'Max']) + table.align['Product'] = 'l' + table.align['Min Price'] = 'r' + table.align['Max Price'] = 'r' + for product in products: + table.add_row([product['product_name'], f"{product['min_price']}€", f"{product['max_price']}€"]) + await update.message.reply_markdown_v2(f'```{table}```') def main()->None: - args = parse_json_file() + products = parse_json_file() - for argument in args: - logging.info(argument) - p = threading.Thread(target=Worker.run, args=(argument, )) + for product in products: + logging.info(product) + p = threading.Thread(target=Worker.run, args=(product, )) p.start() """Start the bot.""" diff --git a/wallamanta/args.json b/wallamanta/args.json deleted file mode 100644 index cdcd410..0000000 --- a/wallamanta/args.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "product_name": "zapatillas", - "distance": "0", - "latitude": "40.4165", - "longitude": "-3.70256", - "condition": "all", - "min_price": "0", - "max_price": "75", - "title_keyword_exclude" : [], - "exclude": [] - }, - { - "product_name": "placa base", - "distance": "0", - "latitude": "40.4165", - "longitude": "-3.70256", - "condition": "all", - "min_price": "0", - "max_price": "75", - "title_keyword_exclude" : [], - "exclude": [] - } -] diff --git a/wallamanta/error_log.txt b/wallamanta/error_log.txt deleted file mode 100644 index 60b44f1..0000000 --- a/wallamanta/error_log.txt +++ /dev/null @@ -1 +0,0 @@ -grafica worker crashed. 'title_key_word_exclude'grafica: Trying to parse 9jd5lyeq726k: portatil toshiba satelite pro i3 r50 .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse wzvlmp1dg46l: torre pc acer para piezas sin el disco duro .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse 8j3xn10dmlj9: Samsung Galaxy J5 2015 , SM-J500FN . Dorado .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse pzp1p4nql9z3: Memoria ram kingston hyperx ddr2 4 gb a 1.066 MH .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse 8z8gxdxyol63: MÓVILES HUAWEI P8 LITE .grafica worker crashed. 'title_key_word_exclude'grafica: Trying to parse 9jd5lyeq726k: portatil toshiba satelite pro i3 r50 .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse e6530nvvpgzo: Dos módulos de memoria RAM .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse qjwdo3ly5wzo: Torre AMD Athlon 64 X2 Dual Core 6000+ .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse wzy72wddwvz5: Memoria ram Kingston 3gb DDR2,800mhz y 667mhz .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse 36enl0qm3y6d: Servicio Técnico Apple Valencia .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse x6q90e52oozy: GALAXY J3 (2016) 8GB negro .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse p617rwnr7565: Memoria Ram DDR3 1600 mHz (2 módulos x 4GB) .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse pj9g19o1d06e: Ram ddr4 1gb .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse wzvlmp1dg46l: torre pc acer para piezas sin el disco duro .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse 8j3xn10dmlj9: Samsung Galaxy J5 2015 , SM-J500FN . Dorado .grafica worker crashed. 'title_key_word_exclude'grafica: Trying to parse mznv5n09ok6n: torre ordenador i5 8GB SSD 240GB HDMI .grafica worker crashed. 'title_key_word_exclude'grafica: Trying to parse nzxyk71xg1j2: ASUS PH-GT1030-O2G GT 1030 2GB GDDR5 .grafica worker crashed. 'title_key_word_exclude'grafica: Trying to parse wzvlmpw7k46l: Ordenador portatil HP Probook (560) .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse e6530nvvpgzo: Dos módulos de memoria RAM .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse qjwdo3ly5wzo: Torre AMD Athlon 64 X2 Dual Core 6000+ .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse wzy72wddwvz5: Memoria ram Kingston 3gb DDR2,800mhz y 667mhz .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse 36enl0qm3y6d: Servicio Técnico Apple Valencia .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse x6q90e52oozy: GALAXY J3 (2016) 8GB negro .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse p617rwnr7565: Memoria Ram DDR3 1600 mHz (2 módulos x 4GB) .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse pj9g19o1d06e: Ram ddr4 1gb .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse wzvlmp1dg46l: torre pc acer para piezas sin el disco duro .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse 8j3xn10dmlj9: Samsung Galaxy J5 2015 , SM-J500FN . Dorado .grafica worker crashed. 'title_key_word_exclude'grafica: Trying to parse nzxyk71xg1j2: ASUS PH-GT1030-O2G GT 1030 2GB GDDR5 .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse e6530nvvpgzo: Dos módulos de memoria RAM .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse qjwdo3ly5wzo: Torre AMD Athlon 64 X2 Dual Core 6000+ .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse wzy72wddwvz5: Memoria ram Kingston 3gb DDR2,800mhz y 667mhz .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse 36enl0qm3y6d: Servicio Técnico Apple Valencia .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse x6q90e52oozy: GALAXY J3 (2016) 8GB negro .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse p617rwnr7565: Memoria Ram DDR3 1600 mHz (2 módulos x 4GB) .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse pj9g19o1d06e: Ram ddr4 1gb .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse wzvlmp1dg46l: torre pc acer para piezas sin el disco duro .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse 8j3xn10dmlj9: Samsung Galaxy J5 2015 , SM-J500FN . Dorado .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse e6530nvvpgzo: Dos módulos de memoria RAM .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse qjwdo3ly5wzo: Torre AMD Athlon 64 X2 Dual Core 6000+ .ram worker crashed. 'title_key_word_exclude'ram: Trying to parse wzy72wddwvz5: Memoria ram Kingston 3gb DDR2,800mhz y 667mhz . \ No newline at end of file diff --git a/wallamanta/products.json b/wallamanta/products.json new file mode 100644 index 0000000..b22c70d --- /dev/null +++ b/wallamanta/products.json @@ -0,0 +1 @@ +[{"product_name": "zapatillas", "distance": "0", "latitude": "40.4165", "longitude": "-3.70256", "condition": "all", "min_price": "0", "max_price": "75", "title_keyword_exclude": [], "exclude": []}, {"product_name": "placa base", "distance": "0", "latitude": "40.4165", "longitude": "-3.70256", "condition": "all", "min_price": "0", "max_price": "75", "title_keyword_exclude": [], "exclude": []}] \ No newline at end of file diff --git a/wallamanta/requirements.txt b/wallamanta/requirements.txt index e0761c6..0248115 100644 --- a/wallamanta/requirements.txt +++ b/wallamanta/requirements.txt @@ -1,2 +1,3 @@ python-telegram-bot==20.1 -requests==2.28.1 \ No newline at end of file +requests==2.28.1 +prettytable==3.6.0 \ No newline at end of file diff --git a/wallamanta/telegram_handler.py b/wallamanta/telegram_handler.py deleted file mode 100644 index 4539cf7..0000000 --- a/wallamanta/telegram_handler.py +++ /dev/null @@ -1,18 +0,0 @@ - -import telegram -import os - -TELEGRAM_CHANNEL_ID = os.getenv("TELEGRAM_CHANNEL_ID") -TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") -LATITUDE = os.getenv("LATITUDE") -LONGITUDE = os.getenv("LONGITUDE") -SLEEP_TIME = os.getenv("SLEEP_TIME") - -class TelegramHandler: - - def run(): - updater = Updater(TELEGRAM_TOKEN) - dispatcher = updater.dispatcher - dispatcher.add_handler()) - updater.start_polling() - updater.idle() diff --git a/wallamanta/worker.py b/wallamanta/worker.py index 4c6da53..365104e 100644 --- a/wallamanta/worker.py +++ b/wallamanta/worker.py @@ -3,6 +3,7 @@ import requests import telegram import os import logging +import json TELEGRAM_CHANNEL_ID = os.getenv("TELEGRAM_CHANNEL_ID") TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") @@ -45,25 +46,33 @@ class Worker: json_data = response.json() return json_data['search_objects'] - def first_run(self, args): + def first_run(self, product): list = [] - articles = self.request(args['product_name'], 0, args['latitude'], args['longitude'], args['distance'], args['condition'], args['min_price'], args['max_price']) + articles = self.request(product['product_name'], 0, product['latitude'], product['longitude'], product['distance'], product['condition'], product['min_price'], product['max_price']) for article in articles: list.insert(0, article['id']) return list - def work(self, args, list): + def work(self, product, list): exec_times = [] bot = telegram.Bot(token = TELEGRAM_TOKEN) - while True: + f = open("products.json") + products = json.load(f) + found = False + for fproduct in products: + if fproduct['product_name'] == product['product_name']: + found = True + break + if not found: + break # Exits worker if product not in list anymore start_time = time.time() - articles = self.request(args['product_name'], 0, args['latitude'], args['longitude'], args['distance'], args['condition'], args['min_price'], args['max_price']) + articles = self.request(product['product_name'], 0, product['latitude'], product['longitude'], product['distance'], product['condition'], product['min_price'], product['max_price']) for article in articles: if not article['id'] in list: logging.info("Found article {}".format(article['title'])) try: - if not self.has_excluded_words(article['title'].lower(), article['description'].lower(), args['exclude']) and not self.is_title_key_word_excluded(article['title'].lower(), args['title_keyword_exclude']): + if not self.has_excluded_words(article['title'].lower(), article['description'].lower(), product['exclude']) and not self.is_title_key_word_excluded(article['title'].lower(), product['title_keyword_exclude']): try: text = f"*Artículo*: {article['title']}\n*Descripción*: {article['description']}\n*Precio*: {article['price']} {article['currency']}\n[Ir al anuncio](https://es.wallapop.com/item/{article['web_slug']})".replace(".", "\.") url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage?chat_id={TELEGRAM_CHANNEL_ID}&text={text}&parse_mode=MarkdownV2" @@ -76,12 +85,12 @@ class Worker: list.insert(0, article['id']) except Exception as e: logging.info("---------- EXCEPTION -----------") - logging.info(f"{args['product_name']} worker crashed. {e}") - logging.info(f"{args['product_name']}: Trying to parse {article['id']}: {article['title']} .\n") + logging.info(f"{product['product_name']} worker crashed. {e}") + logging.info(f"{product['product_name']}: Trying to parse {article['id']}: {article['title']} .\n") time.sleep(SLEEP_TIME) exec_times.append(time.time() - start_time) - logging.info(f"\'{args['product_name']}\' node-> last: {exec_times[-1]} max: {self.get_max_time(exec_times)} avg: {self.get_average_time(exec_times)}") + logging.info(f"\'{product['product_name']}\' node-> last: {exec_times[-1]} max: {self.get_max_time(exec_times)} avg: {self.get_average_time(exec_times)}") def has_excluded_words(self, title, description, excluded_words): for word in excluded_words: @@ -112,15 +121,16 @@ class Worker: largest = i return largest - def run(args): + def run(product): worker = Worker() - list = worker.first_run(args) + list = worker.first_run(product) while True: try: - logging.info(f"Wallapop monitor worker started. Checking for new items containing: \'{args['product_name']}\' with given parameters periodically") - worker.work(args, list) + logging.info(f"Wallapop monitor worker started. Checking for new items containing: \'{product['product_name']}\' with given parameters periodically") + worker.work(product, list) + break except Exception as e: logging.info(f"Exception: {e}") - logging.info(f"{args['product_name']} worker crashed. Restarting worker...") + logging.info(f"{product['product_name']} worker crashed. Restarting worker...") time.sleep(10) - + logging.info(f"Wallapop monitor worker stopped for: \'{product['product_name']}\'") \ No newline at end of file