def subscriptions(update, context): logger.info("[Command /suscripciones]") subscribed_deptos = context.chat_data.get("subscribed_deptos", []) subscribed_cursos = context.chat_data.get("subscribed_cursos", []) sub_deptos_list = ["- <b>({})</b> <i>{} {}</i>".format(x, DEPTS[x][0], DEPTS[x][1]) for x in subscribed_deptos] sub_cursos_list = ["- <b>({}-{})</b> <i>{} en {} {}</i>" .format(x[0], x[1], x[1], DEPTS[x[0]][0], DEPTS[x[0]][1]) for x in subscribed_cursos] result = "<b>Avisos activados:</b> <i>{}</i>\n\n" \ .format("Sí \U00002714 (Detener: /stop)" if context.chat_data.get("enable", False) else "No \U0000274C (Activar: /start)") if sub_deptos_list or sub_cursos_list: result += "Actualmente doy los siguientes avisos para este chat:\n\n" else: result += "Actualmente no tienes suscripciones a ningún departamento o curso.\n" \ "Suscribe avisos con /suscribir_depto o /suscribir_curso." if sub_deptos_list: result += "<b>Avisos por departamento:</b>\n" result += "\n".join(sub_deptos_list) result += "\n\n" if sub_cursos_list: result += "<b>Avisos por curso:</b>\n" result += "\n".join(sub_cursos_list) result += "\n\n" if sub_deptos_list or sub_cursos_list: result += "<i>Puedes desuscribirte con /desuscribir_depto y /desuscribir_curso.</i>" try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text=result)
def stop(update, context): logger.info("[Command /stop]") context.chat_data["enable"] = False try_msg(context.bot, chat_id=update.message.chat_id, text="Ok, dejaré de avisar cambios en el catálogo por este chat. " "Puedes volver a activar los avisos enviándome /start nuevamente." )
def main(): try: with open(path.relpath('config/bot.json'), "r") as bot_config_file: data.config = json.load(bot_config_file) logger.info("Bot config loaded.") except OSError: logger.error("Bot config was not found. Can't initialize.") return try_msg(updater.bot, chat_id=admin_ids[0], text=f'Bot iniciado. Config:\n<pre>{json.dumps(data.config, indent=2)}</pre>', parse_mode="HTML") try: with open(path.relpath('excluded/catalogdata-{}-{}.json'.format(YEAR, SEMESTER)), "r") as datajsonfile: data.current_data = json.load(datajsonfile) logger.info("Data loaded from local, initial check for changes will be made.") check_first = True except OSError: logger.info("No local data was found, initial scraping will be made without checking for changes.") check_first = False data.current_data = scrape_catalog() save_catalog() data.job_check_changes = jq.run_repeating(check_catalog, interval=data.config["changes_check_interval"], first=(1 if check_first else None), name="job_check") data.job_check_changes.enabled = data.config["is_checking_changes"] data.job_check_results = jq.run_repeating(check_results, interval=data.config["results_check_interval"], name="job_results") data.job_check_results.enabled = data.config["is_checking_results"] dp.add_handler(CommandHandler('start', start)) dp.add_handler(CommandHandler('stop', stop)) dp.add_handler(CommandHandler('suscribir_depto', subscribe_depto)) dp.add_handler(CommandHandler('suscribir_curso', subscribe_curso)) dp.add_handler(CommandHandler('desuscribir_depto', unsubscribe_depto)) dp.add_handler(CommandHandler('desuscribir_curso', unsubscribe_curso)) dp.add_handler(CommandHandler('deptos', deptos)) dp.add_handler(CommandHandler('suscripciones', subscriptions)) # Admin commands dp.add_handler(CommandHandler('force_check', force_check, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('get_log', get_log, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('get_chats_data', get_chats_data, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('notification', notification, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('force_notification', force_notification, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('force_check_results', force_check_results, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('enable_check_results', enable_check_results, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('enable_check_changes', enable_check_changes, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('changes_check_interval', changes_check_interval, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('results_check_interval', results_check_interval, filters=Filters.user(admin_ids))) dp.add_handler(CommandHandler('help', admin_help, filters=Filters.user(admin_ids))) updater.start_polling() updater.idle()
def unsubscribe_curso(update, context): logger.info("[Command /desuscribir_curso]") if context.args: deleted = [] notsub = [] failed = [] failed_depto = [] for arg in context.args: try: (d_arg, c_arg) = arg.split("-") except ValueError: failed.append(arg) continue if d_arg in DEPTS: if "subscribed_cursos" not in context.chat_data: context.chat_data["subscribed_cursos"] = [] if (d_arg, c_arg) in context.chat_data["subscribed_cursos"]: context.chat_data["subscribed_cursos"].remove((d_arg, c_arg)) data.persistence.flush() deleted.append((d_arg, c_arg)) else: notsub.append((d_arg, c_arg)) else: failed_depto.append((d_arg, c_arg)) response = "" if deleted: response += "\U0001F6D1 Dejaré de avisarte sobre cambios en:\n<i>{}</i>\n\n" \ .format("\n".join([("- " + x[1] + " de " + DEPTS[x[0]][1] + " ({})".format(x[0])) for x in deleted])) if notsub: response += "\U0001F44D No estás suscrito a\n<i>{}</i>\n\n" \ .format("\n".join([("- " + x[1] + " de " + DEPTS[x[0]][1] + " ({})".format(x[0])) for x in notsub])) if failed_depto: response += "\U0001F914 No pude identificar ningún departamento asociado a:\n<i>{}</i>\n\n" \ .format("\n".join(["- " + x[0] for x in failed_depto])) response += "Puedo recordarte la lista de /deptos que reconozco.\n" if failed: response += "\U0001F914 No pude identificar el par <i>'depto-curso'</i> en:\n<i>{}</i>\n\n"\ .format("\n".join(["- " + str(x) for x in failed])) response += "Guíate por el formato del ejemplo:\n" \ "<i>Ej. /desuscribir_curso 5-CC3001 21-MA1002</i>\n" response += "\nRecuerda que puedes apagar temporalmente todos los avisos usando /stop, " \ "sin perder tus suscripciones" try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text=response) else: try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text="Indícame qué cursos quieres dejar de monitorear.\n" "<i>Ej. /desuscribir_curso 5-CC3001 21-MA1002</i>\n\n" "Para ver las suscripciones de este chat envía /suscripciones\n" "Para ver la lista de códigos de deptos que reconozco envía /deptos\n")
def deptos(update, context): logger.info("[Command /deptos]") deptos_list = ["<b>{}</b> - <i>{} {}</i>".format(x, DEPTS[x][0], DEPTS[x][1]) for x in DEPTS] try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text="Estos son los códigos que representan a cada departamento o área. " "Utilizaré los mismos códigos que usa U-Campus para facilitar la consistencia\n" "\n{}".format("\n".join(deptos_list)))
def enable_check_changes(update, context): if int(update.message.from_user.id) in admin_ids: logger.info("[Command /enable_check_changes from admin %s]", update.message.from_user.id) current = data.job_check_changes.enabled data.job_check_changes.enabled = not current data.config["is_checking_changes"] = not current save_config() notif = "Check changes: {}".format(str(data.config["is_checking_changes"])) try_msg(context.bot, chat_id=admin_ids[0], text=notif ) logger.info(notif)
def notification(update, context): if int(update.message.from_user.id) in admin_ids: logger.info("[Command /notification from admin %s]", update.message.from_user.id) chats_data = dp.chat_data if context.args: message = update.message.text message = message[message.index(" ")+1:].replace("\\", "") for chat_id in chats_data: if chats_data[chat_id].get("enable", False): try_msg(context.bot, chat_id=chat_id, text=message, parse_mode="Markdown", )
def results_check_interval(update, context): if int(update.message.from_user.id) in admin_ids: logger.info("[Command /results_check_interval from admin %s]", update.message.from_user.id) if context.args: try: data.config["results_check_interval"] = int(context.args[0]) except ValueError: logger.error(f'{context.args[0]} is not a valid interval value') return save_config() notif = "Results check interval: {} seconds".format(str(data.config["results_check_interval"])) try_msg(context.bot, chat_id=admin_ids[0], text=notif ) logger.info(notif)
def start(update, context): logger.info("[Command /start]") if context.chat_data.get("enable", False): try_msg(context.bot, chat_id=update.message.chat_id, text="¡Mis avisos para este chat ya están activados! El próximo chequeo será aproximadamente a las " + (data.last_check_time + timedelta(seconds=300)).strftime("%H:%M") + ".\nRecuerda configurar los avisos de este chat usando /suscribir_depto o /suscribir_curso" ) else: context.chat_data["enable"] = True try_msg(context.bot, chat_id=update.message.chat_id, text="A partir de ahora avisaré por este chat si detecto algún cambio en el catálogo de cursos." "\nRecuerda configurar los avisos de este chat usando /suscribir_depto o /suscribir_curso" )
def admin_help(update, context): if int(update.message.from_user.id) in admin_ids: logger.info("[Command /help from admin %s]", update.message.from_user.id) try_msg(context.bot, chat_id=admin_ids[0], text= '/force_check\n' '/get_log\n' '/get_chats_data\n' '/notification\n' '/force_notification\n' '/force_check_results\n' '/enable_check_results\n' '/enable_check_changes\n' '/changes_check_interval\n' '/results_check_interval\n' '/help\n' )
def unsubscribe_depto(update, context): logger.info("[Command /desuscribir_depto]") if context.args: deleted = [] notsuscribed = [] failed = [] for arg in context.args: if arg in DEPTS: if arg in context.chat_data["subscribed_deptos"]: context.chat_data["subscribed_deptos"].remove(arg) data.persistence.flush() deleted.append(arg) else: notsuscribed.append(arg) else: failed.append(arg) response = "" if deleted: response += "\U0001F6D1 Dejaré de avisarte sobre cambios en:\n<i>{}</i>\n\n" \ .format("\n".join(["- " + DEPTS[x][1] + " ({})".format(x) for x in deleted])) if notsuscribed: response += "\U0001F44D No estás suscrito a <i>{}</i>.\n" \ .format("\n".join(["- " + DEPTS[x][1] + " ({})".format(x) for x in notsuscribed])) if failed: response += "\U0001F914 No pude identificar ningún departamento asociado a\n:<i>{}</i>\n\n"\ .format("\n".join(["- " + str(x) for x in failed])) response += "Puedo recordarte la lista de /deptos que reconozco.\n" response += "\nRecuerda que puedes apagar temporalmente todos los avisos usando /stop, " \ "sin perder tus suscripciones" try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text=response) else: try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text="Indícame qué departamentos quieres dejar de monitorear.\n" "<i>Ej. /desuscribir_depto 5 21</i>\n\n" "Para ver las suscripciones de este chat envía /suscripciones\n" "Para ver la lista de códigos de deptos que reconozco envía /deptos\n")
def check_results(context): logger.info("Checking for results...") response = requests.get("https://www.u-cursos.cl/ingenieria/2/novedades_institucion/") soup = BeautifulSoup(response.content, 'html.parser') novedad = soup.find("div", class_="objeto") novedad_id = novedad["data-id"] if novedad_id == data.config["last_novedad_id"]: return data.config["last_novedad_id"] = novedad_id save_config() title = novedad.find("h1").find("a").contents[0] ltitle = title.lower() if "resultado" in ltitle and ( ("modifica" in ltitle) or ("modificación" in ltitle) or ("modificacion" in ltitle) or (("inscripción" in ltitle or "inscripcion" in ltitle) and ( "académica" in ltitle or "academica" in ltitle)) or (" IA" in title) ): chats_data = dp.chat_data message = ("\U0001F575 ¡Detecté una Novedad sobre los resultados de la Inscripción Académica!\n" "Título: <strong>{}</strong>\n\n" "<a href='https://ucampus.uchile.cl/m/fcfm_ia/resultados'>" "\U0001F50D Ver resultados IA</a>\n" "<a href='https://www.u-cursos.cl/ingenieria/2/novedades_institucion'>" "\U0001F381 Ver Novedades</a>".format(title)) for chat_id in chats_data: if chats_data[chat_id].get("enable", False): try_msg(context.bot, chat_id=chat_id, text=message, parse_mode="HTML", disable_web_page_preview=True, )
def subscribe_depto(update, context): logger.info("[Command /suscribir_depto]") if context.args: added = [] already = [] failed = [] for arg in context.args: if arg in DEPTS: if "subscribed_deptos" not in context.chat_data: context.chat_data["subscribed_deptos"] = [] if arg not in context.chat_data["subscribed_deptos"]: context.chat_data["subscribed_deptos"].append(arg) data.persistence.flush() added.append(arg) else: already.append(arg) else: failed.append(arg) response = "" if added: response += "\U0001F4A1 Te avisaré sobre los cambios en:\n<i>{}</i>\n\n" \ .format("\n".join(["- " + DEPTS[x][1] + " ({})".format(x) for x in added])) if already: response += "\U0001F44D Ya te habías suscrito a:\n<i>{}</i>\n\n" \ .format("\n".join(["- " + DEPTS[x][1] + " ({})".format(x) for x in already])) if failed: response += "\U0001F914 No pude identificar ningún departamento asociado a:\n<i>{}</i>\n\n"\ .format("\n".join(["- " + str(x) for x in failed])) response += "Puedo recordarte la lista de /deptos que reconozco.\n" try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text=response) if added and not context.chat_data.get("enable", False): try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text="He registrado tus suscripciones ¡Pero los avisos para este chat están desactivados!.\n" "Actívalos enviándome /start") else: try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text="Debes decirme qué departamentos deseas monitorear.\n<i>Ej. /suscribir_depto 5 21 9</i>\n\n" "Para ver la lista de códigos de deptos que reconozco envía /deptos")
def notify_changes(all_changes, context): chats_data = dp.chat_data changes_dict = {} for d_id in all_changes: changes_str = changes_to_string(all_changes[d_id], d_id) changes_dict[d_id] = changes_str # for chat_id in admin_ids: # DEBUG, send only to admin for chat_id in chats_data: if chats_data[chat_id].setdefault("enable", False): try: subscribed_deptos = chats_data[chat_id].setdefault("subscribed_deptos", []) subscribed_cursos = chats_data[chat_id].setdefault("subscribed_cursos", []) dept_matches = [x for x in subscribed_deptos if x in all_changes] curso_matches = [x for x in subscribed_cursos if (x[0] in all_changes and (x[1] in all_changes[x[0]].get("added", []) or x[1] in all_changes[x[0]].get("deleted", []) or x[1] in all_changes[x[0]].get("modified", {})))] if dept_matches or curso_matches: deptos_messages = [] for d_id in dept_matches: deptos_messages.append("<b>Cambios en {}</b>" "\n{}\n" "<a href='https://ucampus.uchile.cl/m/fcfm_catalogo/" "?semestre={}{}&depto={}'>" "\U0001F50D Ver catálogo</a>" .format(DEPTS[d_id][1], changes_dict[d_id], YEAR, SEMESTER, d_id)) cursos_messages = [] for d_c_id in curso_matches: d_id = d_c_id[0] c_id = d_c_id[1] change_type_str = "" curso_changes_str = "" if c_id in all_changes[d_id].get("added", []): change_type_str = "Curso añadido:" curso_changes_str = added_curso_string(c_id, d_id) elif c_id in all_changes[d_id].get("deleted", []): change_type_str = "Curso eliminado:" curso_changes_str = deleted_curso_string(c_id, d_id) elif c_id in all_changes[d_id].get("modified", {}): change_type_str = "Curso modificado:" curso_changes_str = modified_curso_string(c_id, d_id, all_changes[d_id]["modified"][c_id]) cursos_messages.append("<b>{}</b>" "\n{}\n" "<a href='https://ucampus.uchile.cl/m/fcfm_catalogo/" "?semestre={}{}&depto={}'>" "\U0001F50D Ver catálogo</a>" .format(change_type_str, curso_changes_str, YEAR, SEMESTER, d_id)) t = threading.Thread(target=notify_thread, args=(context, chat_id, deptos_messages, cursos_messages)) t.start() except (Unauthorized, BadRequest): continue except Exception as e: logger.exception("Uncaught exception occurred when notifying chat:") logger.error("Notification process will continue regardless.") try_msg(context.bot, chat_id=admin_ids[0], text="Ayuda, ocurrió un error al notificar y no supe qué hacer uwu.\n{}: {}" .format(str(type(e).__name__), str(e))) continue
def check_catalog(context): try: data.new_data = scrape_catalog() all_changes = {} for d_id in DEPTS: old_cursos_data = data.current_data.get(d_id, {}) new_cursos_data = data.new_data.get(d_id, {}) if len(old_cursos_data) >= 3 and len(new_cursos_data) == 0: data.new_data.update({d_id: old_cursos_data}) logger.exception( f'All cursos in ({d_id}) {DEPTS[d_id][1]} were deleted. Skipping this depto and keeping old information.') try_msg(context.bot, chat_id=admin_ids[0], text=f'Todos los cursos de {DEPTS[d_id][1]} fueron borrados. Me saltaré este departamento y mantendré la información anterior.') continue old_cursos = old_cursos_data.keys() new_cursos = new_cursos_data.keys() ocs = set(old_cursos) ncs = set(new_cursos) added = [x for x in new_cursos if x not in ocs] deleted = [x for x in old_cursos if x not in ncs] inter = [x for x in old_cursos if x in ncs] modified = {} d_data = data.current_data[d_id] d_new_data = data.new_data[d_id] for c_id in inter: mods = {} if d_data[c_id]["nombre"] != d_new_data[c_id]["nombre"]: mods["nombre"] = [d_data[c_id]["nombre"], d_new_data[c_id]["nombre"]] old_secciones = set(d_data[c_id]["secciones"].keys()) new_secciones = set(d_new_data[c_id]["secciones"].keys()) changes_sec = {} added_sec = new_secciones - old_secciones deleted_sec = old_secciones - new_secciones inter_sec = old_secciones & new_secciones modified_sec = {} for s_id in inter_sec: mods_sec = {} s_id = str(s_id) if d_data[c_id]["secciones"][s_id]["profesores"] \ != d_new_data[c_id]["secciones"][s_id]["profesores"]: mods_sec["profesores"] = [d_data[c_id]["secciones"][s_id]["profesores"], d_new_data[c_id]["secciones"][s_id]["profesores"]] if d_data[c_id]["secciones"][s_id]["cupos"] != d_new_data[c_id]["secciones"][s_id]["cupos"]: mods_sec["cupos"] = [d_data[c_id]["secciones"][s_id]["cupos"], d_new_data[c_id]["secciones"][s_id]["cupos"]] if d_data[c_id]["secciones"][s_id]["horarios"] != d_new_data[c_id]["secciones"][s_id]["horarios"]: mods_sec["horarios"] = [d_data[c_id]["secciones"][s_id]["horarios"], d_new_data[c_id]["secciones"][s_id]["horarios"]] if len(mods_sec) > 0: modified_sec[s_id] = mods_sec if len(added_sec) > 0: changes_sec["added"] = added_sec if len(deleted_sec) > 0: changes_sec["deleted"] = deleted_sec if len(modified_sec) > 0: changes_sec["modified"] = modified_sec if len(changes_sec) > 0: mods["secciones"] = changes_sec if len(mods) > 0: modified[c_id] = mods if added or deleted or modified: all_changes[d_id] = {} if added: all_changes[d_id]["added"] = added if deleted: all_changes[d_id]["deleted"] = deleted if modified: all_changes[d_id]["modified"] = modified if len(all_changes) > 0: logger.info("Changes detected on %s", str([x for x in all_changes])) notify_changes(all_changes, context) else: logger.info("No changes detected") data.current_data = data.new_data data.last_check_time = datetime.now() save_catalog() except AllDeletedException as e: logger.error("All cursos were deleted. Aborting check and keeping old information.") try_msg(context.bot, chat_id=admin_ids[0], text="Se han borrado todos los cursos. Se mantendrá la información anterior y se ignorará este check.") except Exception as e: logger.exception("Uncaught exception occurred:") try_msg(context.bot, chat_id=admin_ids[0], text="Ayuda, ocurrió un error y no supe qué hacer uwu.\n{}: {}".format(str(type(e).__name__), str(e)))
def subscribe_curso(update, context): logger.info("[Command /suscribir_curso]") if context.args: added = [] already = [] unknown = [] failed = [] failed_depto = [] for arg in context.args: try: (d_arg, c_arg) = arg.split("-") c_arg = c_arg.upper() except ValueError: failed.append(arg) continue if d_arg in DEPTS: if "subscribed_cursos" not in context.chat_data: context.chat_data["subscribed_cursos"] = [] if (d_arg, c_arg) not in context.chat_data["subscribed_cursos"]: context.chat_data["subscribed_cursos"].append((d_arg, c_arg)) data.persistence.flush() is_curso_known = c_arg in data.current_data[d_arg] if is_curso_known: added.append((d_arg, c_arg)) else: unknown.append((d_arg, c_arg)) else: already.append((d_arg, c_arg)) else: failed_depto.append((d_arg, c_arg)) response = "" if added: response += "\U0001F4A1 Te avisaré sobre cambios en:\n<i>{}</i>\n\n" \ .format("\n".join(["- " + (x[1] + " de " + DEPTS[x[0]][1] + " ({})".format(x[0])) for x in added])) if unknown: response += "\U0001F4A1 Actualmente no tengo registros de:\n<i>{}</i>\n" \ .format("\n".join(["- " + (x[1] + " en " + DEPTS[x[0]][1] + " ({})".format(x[0])) for x in unknown])) response += "Te avisaré si aparece algún curso con ese código en ese depto.\n\n" if already: response += "\U0001F44D Ya estabas suscrito a:\n<i>{}</i>.\n\n" \ .format("\n".join(["- " + (x[1] + " de " + DEPTS[x[0]][1] + " ({})".format(x[0])) for x in already])) if failed_depto: response += "\U0001F914 No pude identificar ningún departamento asociado a:\n<i>{}</i>\n\n" \ .format("\n".join(["- " + x[0] for x in failed_depto])) response += "Puedo recordarte la lista de /deptos que reconozco.\n" if failed: response += "\U0001F914 No pude identificar el par <i>'depto-curso'</i> en:\n<i>{}</i>\n\n"\ .format("\n".join(["- " + str(x) for x in failed])) response += "Guíate por el formato del ejemplo:\n" \ "<i>Ej. /suscribir_curso 5-CC3001 21-MA1002</i>\n" try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text=response) if (added or unknown) and not context.chat_data.get("enable", False): try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text="He registrado tus suscripciones ¡Pero los avisos para este chat están desactivados!\n" "Actívalos enviándome /start") else: try_msg(context.bot, chat_id=update.message.chat_id, parse_mode="HTML", text="Debes decirme qué cursos deseas monitorear en la forma <i>'depto-curso'</i> para registrarlo.\n" "<i>Ej. /suscribir_curso 5-CC3001 21-MA1002</i>\n\n" "Para ver la lista de códigos de deptos que reconozco envía /deptos")