def pa_detail_handler(update, context): """Handler for the price agent detail menu""" cbq = update.callback_query user_id = cbq.from_user.id menu, action, entity_id, entity_type_str = cbq.data.split("_") entity_type = EntityType(int(entity_type_str)) entity = core.get_entity(entity_id, entity_type) user = core.get_user_by_id(user_id) if action == "show": cbq.edit_message_text(text="{link_name} kostet aktuell {price}".format( link_name=link(entity.url, entity.name), price=bold(price(entity.price, signed=False))), reply_markup=get_entity_keyboard(entity.TYPE, entity.entity_id), parse_mode=ParseMode.HTML, disable_web_page_preview=True) cbq.answer() elif action == "delete": core.unsubscribe_entity(user, entity) callback_data = 'm04_subscribe_{id}_{type}'.format(id=entity.entity_id, type=entity.TYPE.value) keyboard = [[InlineKeyboardButton("Rückgängig", callback_data=callback_data)]] reply_markup = InlineKeyboardMarkup(keyboard) text = "Preisagent für '{0}' wurde gelöscht!".format(link(entity.url, entity.name)) cbq.edit_message_text(text=text, reply_markup=reply_markup, parse_mode=ParseMode.HTML, disable_web_page_preview=True) cbq.answer(text="Preisagent für wurde gelöscht!") elif action == "subscribe": entity_info = EntityType.get_type_article_name(entity.TYPE) try: core.subscribe_entity(user, entity) text = "Du hast {article} {entity_name} {link_name} erneut abboniert!".format( article=entity_info.get("article"), entity_name=entity_info.get("name"), link_name=link(entity.url, entity.name)) cbq.edit_message_text(text=text, parse_mode=ParseMode.HTML, disable_web_page_preview=True) cbq.answer(text="{} erneut abboniert".format(entity_info.get("name"))) except AlreadySubscribedException: text = "{} bereits abboniert!".format(entity_info.get("name")) cbq.edit_message_text(text=text, parse_mode=ParseMode.HTML, disable_web_page_preview=True) cbq.answer(text=text) elif action == "history": entity_price_history(update, context) else: logger.warning("A user tried to use an unimplemented method: '{}'".format(action)) cbq.answer(text="Die gewählte Funktion ist noch nicht implementiert!")
def add_entity(update, context): msg = update.message text = update.message.text t_user = update.effective_user logger.info("Adding new entity for user '{}'".format(t_user.id)) reply_markup = InlineKeyboardMarkup([[cancel_button]]) user = User(user_id=t_user.id, first_name=t_user.first_name, last_name=t_user.last_name, username=t_user.username, lang_code=t_user.language_code) core.add_user_if_new(user) try: entity_type = core.get_type_by_url(text) url = core.get_e_url(text, entity_type) logger.info("Valid URL for new entity is '{}'".format(url)) except InvalidURLException: logger.warning("Invalid url '{}' sent by user {}!".format(text, t_user)) msg.reply_text(text="Die URL ist ungültig!", reply_markup=reply_markup) return try: if entity_type == EntityType.WISHLIST: entity = Wishlist.from_url(url) elif entity_type == EntityType.PRODUCT: entity = Product.from_url(url) else: raise ValueError("EntityType '{}' not found!".format(entity_type)) except HTTPError as e: logger.error(e) if e.response.status_code == 403: msg.reply_text(text="Die URL ist nicht öffentlich einsehbar, daher wurde kein neuer Preisagent erstellt!") elif e.response.status_code == 429: msg.reply_text(text="Entschuldige, ich bin temporär bei Geizhals blockiert und kann keine Preise auslesen. Bitte probiere es später noch einmal.") except (ValueError, Exception) as e: # Raised when price could not be parsed logger.error(e) msg.reply_text(text="Name oder Preis konnte nicht ausgelesen werden! Preisagent wurde nicht erstellt!") else: core.add_entity_if_new(entity) try: logger.debug("Subscribing to entity.") core.subscribe_entity(user, entity) entity_data = EntityType.get_type_article_name(entity_type) msg.reply_html("Preisagent für {article} {type} {link_name} erstellt! Aktueller Preis: {price}".format( article=entity_data.get("article"), type=entity_data.get("name"), link_name=link(entity.url, entity.name), price=bold(price(entity.price, signed=False))), disable_web_page_preview=True) context.user_data["state"] = State.IDLE except AlreadySubscribedException: logger.debug("User already subscribed!") msg.reply_text("Du hast bereits einen Preisagenten für diese URL! Bitte sende mir eine andere URL.", reply_markup=InlineKeyboardMarkup([[cancel_button]]))
def add_product(bot, update): text = update.message.text user = update.message.from_user logger.info("Adding new product for user '{}'".format(user.id)) reply_markup = InlineKeyboardMarkup([[cancel_button]]) add_user_if_new(user) try: url = get_p_url(text) logger.info("Valid URL for new product is '{}'".format(url)) except InvalidURLException: logger.warning("Invalid url '{}' sent by user {}!".format(text, user)) bot.sendMessage(chat_id=user.id, text="Die URL ist ungültig!", reply_markup=reply_markup) return try: product = Product.from_url(url) except HTTPError as e: logger.error(e) if e.code == 403: bot.sendMessage(chat_id=user.id, text="Das Produkt ist nicht zugänglich! Preisagent wurde nicht erstellt!") elif e.code == 429: bot.sendMessage(chat_id=user.id, text="Entschuldige, ich bin temporär bei Geizhals blockiert und kann keine Preise auslesen. Bitte probiere es später noch einmal.") except ValueError as valueError: # Raised when price could not be parsed logger.error(valueError) bot.sendMessage(chat_id=user.id, text="Name oder Preis konnte nicht ausgelesen werden! Preisagent wurde nicht erstellt!") except Exception as e: logger.error(e) bot.sendMessage(chat_id=user.id, text="Name oder Preis konnte nicht ausgelesen werden! Wunschliste nicht erstellt!") else: add_product_if_new(product) try: logger.debug("Subscribing to product.") subscribe_entity(user, product) bot.sendMessage(user.id, "Preisagent für das Produkt {link_name} erstellt! Aktueller Preis: {price}".format( link_name=link(product.url, product.name), price=bold(price(product.price, signed=False))), parse_mode="HTML", disable_web_page_preview=True) rm_state(user.id) except AlreadySubscribedException: logger.debug("User already subscribed!") bot.sendMessage(user.id, "Du hast bereits einen Preisagenten für dieses Produkt! Bitte sende mir eine andere URL.", reply_markup=InlineKeyboardMarkup([[cancel_button]]))
def add_wishlist(bot, update): text = update.message.text user = update.message.from_user reply_markup = InlineKeyboardMarkup([[cancel_button]]) add_user_if_new(user) try: url = get_wl_url(text) except InvalidURLException: logger.debug("Invalid url '{}'!".format(text)) bot.sendMessage(chat_id=user.id, text="Die URL ist ungültig!", reply_markup=reply_markup) return # Check if website is parsable! try: wishlist = Wishlist.from_url(url) except HTTPError as e: logger.error(e) if e.code == 403: bot.sendMessage(chat_id=user.id, text="Wunschliste ist nicht öffentlich! Wunschliste nicht hinzugefügt!") elif e.code == 429: bot.sendMessage(chat_id=user.id, text="Entschuldige, ich bin temporär bei Geizhals blockiert und kann keine Preise auslesen. Bitte probiere es später noch einmal.") except ValueError as valueError: # Raised when price could not be parsed logger.error(valueError) bot.sendMessage(chat_id=user.id, text="Name oder Preis konnte nicht ausgelesen werden! Preisagent wurde nicht erstellt!") except Exception as e: logger.error(e) bot.sendMessage(chat_id=user.id, text="Name oder Preis konnte nicht ausgelesen werden! Preisagent wurde nicht erstellt!") else: add_wishlist_if_new(wishlist) try: logger.debug("Subscribing to wishlist.") subscribe_entity(user, wishlist) bot.sendMessage(user.id, "Preisagent für die Wunschliste {link_name} erstellt! Aktueller Preis: {price}".format( link_name=link(wishlist.url, wishlist.name), price=bold(price(wishlist.price, signed=False))), parse_mode="HTML", disable_web_page_preview=True) rm_state(user.id) except AlreadySubscribedException as ase: logger.debug("User already subscribed!") bot.sendMessage(user.id, "Du hast bereits einen Preisagenten für diese Wunschliste! Bitte sende mir eine andere URL.", reply_markup=InlineKeyboardMarkup([[cancel_button]]))
def notify_user(bot, user_id, entity, old_price): """Notify a user of price changes""" diff = entity.price - old_price if diff > 0: emoji = "📈" change = "teurer" else: emoji = "📉" change = "billiger" logger.info("Notifying user {}!".format(user_id)) message = "Der Preis von {link_name} hat sich geändert: {price}\n\n" \ "{emoji} {diff} {change}".format(link_name=link(entity.url, entity.name), price=bold(price(entity.price, signed=False)), emoji=emoji, diff=bold(price(diff)), change=change) bot.sendMessage(user_id, message, parse_mode=ParseMode.HTML, disable_web_page_preview=True)
def entity_price_history(update, _): """Handles button clicks on the price history button""" cbq = update.callback_query data = cbq.data if "_" in data: menu, action, entity_id, entity_type_value = data.split("_") entity_type = EntityType(int(entity_type_value)) else: logger.error("Error before unpacking. There is no '_' in the callback query data!") text = "An error occurred! This error was logged." cbq.message.reply_text(text=text, parse_mode=ParseMode.HTML, disable_web_page_preview=True) cbq.answer(text=text) return entity = core.get_entity(entity_id, entity_type) items = core.get_price_history(entity) from geizhals.charts.dataset import Dataset ds = Dataset(entity.name) for p, timestamp, name in items: ds.add_price(price=p, timestamp=timestamp) if len(items) <= 3 or len(ds.days) <= 3: cbq.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup([])) cbq.message.reply_text("Entschuldige, leider habe ich nicht genügend Daten für diesen Preisagenten, um einen Preisverlauf anzeigen zu können! " "Schau einfach in ein paar Tagen nochmal vorbei!") return chart = ds.get_chart() file_name = "{}.png".format(entity.entity_id) logger.info("Generated new chart '{}' for user '{}'".format(file_name, cbq.from_user.id)) file = io.BytesIO(chart) cbq.message.reply_photo(photo=file) cbq.message.edit_text("Hier ist der Preisverlauf für {}".format(link(entity.url, entity.name)), reply_markup=InlineKeyboardMarkup([]), parse_mode=ParseMode.HTML, disable_web_page_preview=True) cbq.answer()
def test_link(self): text = "name" link = "www.example.com" self.assertEqual(formatter.link(link, text), "<a href=\"www.example.com\">name</a>")
def check_for_price_update(context): """Check if the price of any subscribed wishlist or product was updated""" bot = context.bot logger.debug("Checking for updates!") bot = context.bot entities = core.get_all_entities_with_subscribers() # Check all entities for price updates for entity in entities: logger.debug("URL is '{}'".format(entity.url)) old_price = entity.price old_name = entity.name try: new_price = entity.get_current_price() new_name = entity.get_current_name() except HTTPError as e: if e.response.status_code == 403: logger.error("Entity is not public!") entity_type_data = EntityType.get_type_article_name(entity.TYPE) entity_hidden = "{article} {type} {link_name} ist leider nicht mehr einsehbar. " \ "Ich entferne diesen Preisagenten!".format(article=entity_type_data.get("article").capitalize(), type=entity_type_data.get("name"), link_name=link(entity.url, entity.name)) for user_id in core.get_entity_subscribers(entity): user = core.get_user_by_id(user_id) bot.send_message(user_id, entity_hidden, parse_mode=ParseMode.HTML) core.unsubscribe_entity(user, entity) core.rm_entity(entity) except (ValueError, Exception) as e: logger.error("Exception while checking for price updates! {}".format(e)) else: if old_name != new_name: core.update_entity_name(entity, new_name) # Make sure to update the price no matter if it changed. Helps for generating charts entity.price = new_price core.update_entity_price(entity, new_price) if old_price == new_price: continue entity_subscribers = core.get_entity_subscribers(entity) for user_id in entity_subscribers: # Notify each subscriber try: notify_user(bot, user_id, entity, old_price) except Unauthorized as e: if e.message == "Forbidden: user is deactivated": logger.info("Removing user from db, because account was deleted.") elif e.message == "Forbidden: bot was blocked by the user": logger.info("Removing user from db, because they blocked the bot.") core.delete_user(user_id)
def callback_handler_f(bot, update): user_id = update.callback_query.from_user.id message_id = update.callback_query.message.message_id callback_query_id = update.callback_query.id user = get_user_by_id(user_id) if user is None: return data = update.callback_query.data if "_" in data: # TODO Input validation! action, entity_id, entity_type_value = data.split("_") entity_type = EntityType(int(entity_type_value)) else: action = data entity_id = None entity_type = None if entity_id: """Check if it's just a command or if an ID/type is passed""" if entity_type == EntityType.WISHLIST: wishlist_id = entity_id try: wishlist = get_wishlist(wishlist_id) except WishlistNotFoundException: invalid_wl_text = "Die Wunschliste existiert nicht!" bot.answerCallbackQuery(callback_query_id=callback_query_id, text=invalid_wl_text) bot.editMessageText(chat_id=user_id, message_id=message_id, text=invalid_wl_text) return if action == "delete": unsubscribe_entity(user, wishlist) callback_data = 'subscribe_{id}_{type}'.format(id=wishlist_id, type=EntityType.WISHLIST.value) keyboard = [ [InlineKeyboardButton("Rückgängig", callback_data=callback_data)]] reply_markup = InlineKeyboardMarkup(keyboard) bot.editMessageText(chat_id=user_id, message_id=message_id, text="Preisagent für die Wunschliste {link_name} wurde gelöscht!".format( link_name=link(wishlist.url, wishlist.name)), reply_markup=reply_markup, parse_mode="HTML", disable_web_page_preview=True) bot.answerCallbackQuery(callback_query_id=callback_query_id, text="Preisagent für die Wunschliste wurde gelöscht!") elif action == "show": bot.editMessageText(chat_id=user_id, message_id=message_id, text="Die Wunschliste {link_name} kostet aktuell {price}".format( link_name=link(wishlist.url, wishlist.name), price=bold(price(wishlist.price, signed=False))), reply_markup=get_entity_keyboard(EntityType.WISHLIST, wishlist.id, "showWishlists"), parse_mode="HTML", disable_web_page_preview=True) bot.answerCallbackQuery(callback_query_id=callback_query_id) elif action == "subscribe": try: subscribe_entity(user, wishlist) text = "Du hast die Wunschliste {link_name} erneut abboniert!".format( link_name=link(wishlist.url, wishlist.name)) bot.editMessageText(chat_id=user_id, message_id=message_id, text=text, parse_mode="HTML", disable_web_page_preview=True) bot.answerCallbackQuery(callback_query_id=callback_query_id, text="Wunschliste erneut abboniert") except AlreadySubscribedException: text = "Wunschliste bereits abboniert!" bot.editMessageText(chat_id=user_id, message_id=message_id, text=text, parse_mode="HTML", disable_web_page_preview=True) bot.answerCallbackQuery(callback_query_id=callback_query_id, text=text) elif entity_type == EntityType.PRODUCT: product_id = entity_id try: product = get_product(product_id) except ProductNotFoundException: invalid_p_text = "Das Produkt existiert nicht!" bot.answerCallbackQuery(callback_query_id=callback_query_id, text=invalid_p_text) bot.editMessageText(chat_id=user_id, message_id=message_id, text=invalid_p_text) return if action == "delete": unsubscribe_entity(user, product) callback_data = 'subscribe_{id}_{type}'.format(id=product_id, type=EntityType.PRODUCT.value) keyboard = [[InlineKeyboardButton("Rückgängig", callback_data=callback_data)]] reply_markup = InlineKeyboardMarkup(keyboard) bot.editMessageText(chat_id=user_id, message_id=message_id, text="Preisagent für das Produkt {link_name} wurde gelöscht!".format( link_name=link(product.url, product.name)), reply_markup=reply_markup, parse_mode="HTML", disable_web_page_preview=True) bot.answerCallbackQuery(callback_query_id=callback_query_id, text="Preisagent für das Produkt wurde gelöscht!") elif action == "show": bot.editMessageText(chat_id=user_id, message_id=message_id, text="Das Produkt {link_name} kostet aktuell {price}".format( link_name=link(product.url, product.name), price=bold(price(product.price, signed=False))), reply_markup=get_entity_keyboard(EntityType.PRODUCT, product.id, "showProducts"), parse_mode="HTML", disable_web_page_preview=True) bot.answerCallbackQuery(callback_query_id=callback_query_id) elif action == "subscribe": try: subscribe_entity(user, product) text = "Du hast das Produkt {link_name} erneut abboniert!".format( link_name=link(product.url, product.name)) bot.editMessageText(chat_id=user_id, message_id=message_id, text=text, parse_mode="HTML", disable_web_page_preview=True) bot.answerCallbackQuery(callback_query_id=callback_query_id, text="Produkt erneut abboniert") except AlreadySubscribedException: text = "Produkt bereits abboniert!" bot.editMessageText(chat_id=user_id, message_id=message_id, text=text, parse_mode="HTML", disable_web_page_preview=True) bot.answerCallbackQuery(callback_query_id=callback_query_id, text=text) elif action == "newPriceAgent": keyboard = [[InlineKeyboardButton("Wunschliste", callback_data='addWishlist'), InlineKeyboardButton("Produkt", callback_data='addProduct')]] bot.editMessageText(chat_id=user_id, message_id=message_id, text="Wofür möchtest du einen Preisagenten einrichten?", reply_markup=InlineKeyboardMarkup(keyboard)) elif action == "myPriceAgents": keyboard = [[InlineKeyboardButton("Wunschlisten", callback_data='showWishlists'), InlineKeyboardButton("Produkte", callback_data='showProducts')]] bot.editMessageText(chat_id=user_id, message_id=message_id, text="Welche Preisagenten möchtest du einsehen?", reply_markup=InlineKeyboardMarkup(keyboard)) elif action == "cancel": """Reset the user's state""" rm_state(user_id) text = "Okay, Ich habe die Aktion abgebrochen!" bot.editMessageText(chat_id=user_id, message_id=message_id, text=text) bot.answerCallbackQuery(callback_query_id=callback_query_id, text=text) elif action == "addWishlist": if get_wishlist_count(user_id) >= MAX_WISHLISTS: bot.editMessageText(chat_id=user_id, message_id=message_id, text="Du kannst zu maximal 5 Wunschlisten Benachrichtigungen bekommen. " "Entferne doch eine Wunschliste, die du nicht mehr benötigst.", reply_markup=get_entities_keyboard("delete", get_wishlists_for_user(user_id), prefix_text="❌ ", cancel=True)) return set_state(user_id, STATE_SEND_WL_LINK) bot.editMessageText(chat_id=user_id, message_id=message_id, text="Bitte sende mir eine URL einer Wunschliste!", reply_markup=InlineKeyboardMarkup([[cancel_button]])) bot.answerCallbackQuery(callback_query_id=callback_query_id) elif action == "addProduct": if get_product_count(user_id) >= MAX_PRODUCTS: bot.editMessageText(chat_id=user_id, message_id=message_id, text="Du kannst zu maximal 5 Wunschlisten Benachrichtigungen bekommen. " "Entferne doch eine Wunschliste, die du nicht mehr benötigst.", reply_markup=get_entities_keyboard("delete", get_products_for_user(user_id), prefix_text="❌ ", cancel=True)) return set_state(user_id, STATE_SEND_P_LINK) bot.editMessageText(chat_id=user_id, message_id=message_id, text="Bitte sende mir eine URL eines Produkts!", reply_markup=InlineKeyboardMarkup([[cancel_button]])) bot.answerCallbackQuery(callback_query_id=callback_query_id) elif action == "showWishlists": wishlists = get_wishlists_for_user(user_id) if len(wishlists) == 0: bot.editMessageText(chat_id=user_id, message_id=message_id, text="Du hast noch keinen Preisagenten für eine Wunschliste angelegt!") return keyboard = get_entities_keyboard("show", wishlists) bot.answerCallbackQuery(callback_query_id=callback_query_id) bot.editMessageText(chat_id=user_id, message_id=message_id, text="Das sind deine Preisagenten für deine Wunschlisten:", reply_markup=keyboard) elif action == "showProducts": products = get_products_for_user(user_id) if len(products) == 0: bot.editMessageText(chat_id=user_id, message_id=message_id, text="Du hast noch keinen Preisagenten für ein Produkt angelegt!") return keyboard = get_entities_keyboard("show", products) bot.editMessageText(chat_id=user_id, message_id=message_id, text="Das sind deine Preisagenten für deine Produkte:", reply_markup=keyboard)
def check_for_price_update(bot, job): """Check if the price of any subscribed wishlist or product was updated""" logger.debug("Checking for updates!") entities = get_all_entities_with_subscribers() # Check all entities for price updates for entity in entities: logger.debug("URL is '{}'".format(entity.url)) old_price = entity.price old_name = entity.name try: new_price = entity.get_current_price() new_name = entity.get_current_name() except HTTPError as e: if e.code == 403: logger.error("Entity is not public!") if entity.TYPE == EntityType.PRODUCT: entity_hidden = "Das Produkt {link_name} ist leider nicht mehr einsehbar. " \ "Ich entferne diesen Preisagenten!".format(link_name=link(entity.url, entity.name)) elif entity.TYPE == EntityType.WISHLIST: entity_hidden = "Die Wunschliste {link_name} ist leider nicht mehr einsehbar. " \ "Ich entferne diesen Preisagent.".format(link_name=link(entity.url, entity.name)) else: raise ValueError("No such entity type '{}'!".format(entity.TYPE)) for user_id in get_entity_subscribers(entity): user = get_user_by_id(user_id) bot.send_message(user_id, entity_hidden, parse_mode="HTML") unsubscribe_entity(user, entity) rm_entity(entity) except ValueError as e: logger.error("ValueError while checking for price updates! {}".format(e)) except Exception as e: logger.error("Exception while checking for price updates! {}".format(e)) else: if old_price != new_price: entity.price = new_price update_entity_price(entity, new_price) entity_subscribers = get_entity_subscribers(entity) for user_id in entity_subscribers: # Notify each subscriber try: notify_user(bot, user_id, entity, old_price) except Unauthorized as e: if e.message == "Forbidden: user is deactivated": logging.info("Removed user from db, because account was deleted.") delete_user(user_id) if old_name != new_name: update_entity_name(entity, new_name)