Bot de Telegram para una selección personalizada de artículos de Habr

Para preguntas como "¿por qué?" Hay un artículo más antiguo. Natural Geektimes: hacer el espacio más limpio.

Hay muchos artículos, por razones subjetivas, algunos no me gustan y otros, por el contrario, es una lástima omitirlos. Me gustaría optimizar este proceso y ahorrar tiempo.

El artículo anterior sugirió un enfoque de secuencias de comandos en el navegador, pero realmente no me gustó (aunque lo he usado antes) por las siguientes razones:

  • Para diferentes navegadores en su computadora/teléfono, debe configurarlo nuevamente, si es posible.
  • Un filtrado estricto por autores no siempre es conveniente.
  • El problema de los autores cuyos artículos no quieres perderte, aunque se publiquen una vez al año, no se ha solucionado.

El filtrado integrado en el sitio en función de las calificaciones de los artículos no siempre es conveniente, ya que los artículos altamente especializados, a pesar de su valor, pueden recibir una calificación bastante modesta.

Al principio quería generar un feed RSS (o incluso varios), dejando allí sólo cosas interesantes. Pero al final resultó que leer RSS no parecía muy cómodo: en cualquier caso, para comentar/votar un artículo/añadirlo a favoritos, hay que pasar por el navegador. Por eso escribí un bot de Telegram que me envía artículos interesantes en un mensaje personal. El propio Telegram hace hermosas vistas previas de ellos, que, combinadas con información sobre el autor/calificación/vistas, parecen bastante informativas.

Bot de Telegram para una selección personalizada de artículos de Habr

Debajo del corte se encuentran detalles como las características de la obra, el proceso de escritura y las soluciones técnicas.

Brevemente sobre el bot

Repositorio: https://github.com/Kright/habrahabr_reader

Bot en telegrama: https://t.me/HabraFilterBot

El usuario establece una calificación adicional para etiquetas y autores. Después de eso, se aplica un filtro a los artículos: se suman la calificación del artículo en Habré, la calificación de los usuarios del autor y el promedio de las calificaciones de los usuarios por etiqueta. Si el monto es mayor que un umbral especificado por el usuario, entonces el artículo pasa el filtro.

Un objetivo secundario de escribir un bot era ganar diversión y experiencia. Además, periódicamente me recordaba a mí mismo que no soy google, y por lo tanto muchas cosas se hacen de la manera más simple e incluso primitiva posible. Sin embargo, esto no impidió que el proceso de redacción del bot tardara tres meses.

Afuera era verano

Julio estaba terminando y decidí escribir un bot. Y no solo, sino con un amigo que dominaba Scala y quería escribir algo sobre ello. El comienzo parecía prometedor: un equipo cortaría el código, la tarea parecía fácil y pensé que en un par de semanas o un mes el bot estaría listo.

A pesar de que yo mismo he estado escribiendo código en la roca de vez en cuando durante los últimos años, normalmente nadie ve ni mira este código: proyectos favoritos, probar algunas ideas, preprocesar datos, dominar algunos conceptos de FP. Estaba realmente interesado en cómo se escribe código en un equipo, porque el código en roca se puede escribir de maneras muy diferentes.

¿Qué pudo haber pasado? tan? Sin embargo, no apresuremos las cosas.
Todo lo que sucede se puede rastrear utilizando el historial de confirmaciones.

Un conocido creó un repositorio el 27 de julio, pero no hizo nada más, así que comencé a escribir código.

julio 30

Brevemente: escribí un análisis del feed rss de Habr.

  • com.github.pureconfig para leer configuraciones de tipo seguro directamente en clases de casos (resultó ser muy conveniente)
  • scala-xml para leer xml: como inicialmente quería escribir mi propia implementación para el feed rss, y el feed rss está en formato xml, utilicé esta biblioteca para analizar. De hecho, también apareció el análisis de RSS.
  • scalatest para pruebas. Incluso para proyectos pequeños, escribir pruebas ahorra tiempo; por ejemplo, al depurar el análisis xml, es mucho más fácil descargarlo en un archivo, escribir pruebas y corregir errores. Cuando más tarde apareció un error al analizar un HTML extraño con caracteres utf-8 no válidos, resultó más conveniente colocarlo en un archivo y agregar una prueba.
  • actores de Akka. Objetivamente, no eran necesarios en absoluto, pero el proyecto fue escrito por diversión, quería probarlos. Como resultado, estoy dispuesto a decir que me gustó. La idea de POO se puede ver desde el otro lado: hay actores que intercambian mensajes. Lo que es más interesante es que puedes (y debes) escribir código de tal manera que el mensaje no llegue o no se procese (en términos generales, cuando la cuenta se ejecuta en una sola computadora, los mensajes no deben perderse). Al principio me estaba rascando la cabeza y había basura en el código con actores suscribiéndose entre sí, pero al final logré crear una arquitectura bastante simple y elegante. El código dentro de cada actor puede considerarse de un solo subproceso; cuando un actor falla, el acca lo reinicia; el resultado es un sistema bastante tolerante a fallas.

9 agosto

Agregué al proyecto scala-scrapper para analizar páginas html de Habr (para extraer información como la calificación del artículo, el número de marcadores, etc.).

Y gatos. Los de la roca.

Bot de Telegram para una selección personalizada de artículos de Habr

Luego leí un libro sobre bases de datos distribuidas, me gustó la idea de CRDT (tipo de datos replicados sin conflictos, https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type, Hablar), así que publiqué una clase tipo de un semigrupo conmutativo para obtener información sobre el artículo sobre Habré.

De hecho, la idea es muy simple: tenemos contadores que cambian monótonamente. El número de promociones está creciendo gradualmente, al igual que el número de ventajas (así como el número de desventajas). Si tengo dos versiones de información sobre un artículo, entonces puedo "fusionarlas en una": el estado del contador que es más grande se considera más relevante.

Un semigrupo significa que dos objetos con información sobre un artículo se pueden fusionar en uno. Conmutativo significa que puedes fusionar A + B y B + A, el resultado no depende del orden y al final quedará la versión más nueva. Por cierto, aquí también hay asociatividad.

Por ejemplo, como estaba previsto, rss después del análisis proporcionó información ligeramente debilitada sobre el artículo, sin métricas como el número de visitas. Luego, un actor especial tomó información sobre los artículos y corrió a las páginas html para actualizarla y fusionarla con la versión anterior.

En términos generales, como en akka, no había necesidad de esto, simplemente podías almacenar updateDate para el artículo y tomar uno más nuevo sin fusiones, pero el camino de la aventura me guió.

12 agosto

Empecé a sentirme más libre y, sólo por diversión, convertí cada charla en un actor independiente. En teoría, un actor pesa alrededor de 300 bytes y se pueden crear en millones, por lo que este es un enfoque completamente normal. Me parece que la solución resultó bastante interesante:

Un actor hacía de puente entre el servidor de Telegram y el sistema de mensajes de Akka. Simplemente recibió mensajes y los envió al actor de chat deseado. El actor del chat podría enviar algo como respuesta y se enviaría de vuelta al telegrama. Lo que fue muy conveniente es que este actor resultó ser lo más simple posible y solo contenía la lógica para responder a los mensajes. Por cierto, en cada chat llegó información sobre nuevos artículos, pero nuevamente no veo ningún problema en esto.

En general, el bot ya estaba funcionando, respondiendo mensajes, almacenando una lista de artículos enviados al usuario, y ya pensaba que el bot estaba casi listo. Poco a poco agregué pequeñas funciones como normalizar los nombres y etiquetas de los autores (reemplazando “sd f” por “s_d_f”).

Sólo quedaba una cosa pequeño pero — el estado no se salvó en ninguna parte.

Todo salio mal

Quizás hayas notado que escribí el bot principalmente solo. Entonces, el segundo participante se involucró en el desarrollo y aparecieron los siguientes cambios en el código:

  • MongoDB parecía almacenar el estado. Al mismo tiempo, los registros del proyecto estaban rotos, porque por alguna razón Monga comenzó a enviarles spam y algunas personas simplemente los desactivaron globalmente.
  • El actor de bridge en Telegram se transformó hasta quedar irreconocible y comenzó a analizar mensajes él mismo.
  • Los actores de los chats fueron eliminados sin piedad y, en su lugar, fueron reemplazados por un actor que ocultó toda la información sobre todos los chats a la vez. Por cada estornudo, este actor se metía en problemas. Bueno, sí, como cuando se actualiza información sobre un artículo, es difícil enviarla a todos los actores del chat (somos como Google, millones de usuarios esperan un millón de artículos en el chat para cada uno), pero cada vez que se actualiza el chat, Es normal entrar en Monga. Como me di cuenta mucho después, la lógica de funcionamiento de los chats también se cortó por completo y en su lugar apareció algo que no funcionaba.
  • No queda rastro de las clases tipo.
  • Ha aparecido una lógica malsana entre los actores con sus suscripciones mutuas, lo que ha llevado a una condición racial.
  • Estructuras de datos con campos de tipo. Option[Int] convertido en Int con valores predeterminados mágicos como -1. Más tarde me di cuenta de que mongoDB almacena json y no hay nada de malo en almacenarlo allí. Option bueno, o al menos analizar -1 como Ninguno, pero en ese momento no lo sabía y tomé mi palabra de que "así es como debería ser". No escribí ese código y no me molesté en cambiarlo por el momento.
  • Descubrí que mi dirección IP pública tiende a cambiar y cada vez tenía que agregarla a la lista blanca de Mongo. Lancé el bot localmente, Monga estaba en algún lugar de los servidores de Monga como empresa.
  • De repente, la normalización de las etiquetas y el formato de los mensajes de telegramas desapareció. (Hmm, ¿por qué sería eso?)
  • Me gustó que el estado del bot se almacena en una base de datos externa y, cuando se reinicia, sigue funcionando como si nada hubiera pasado. Sin embargo, esta fue la única ventaja.

La segunda persona no tenía mucha prisa y todos estos cambios aparecieron en un gran montón ya a principios de septiembre. No aprecié de inmediato la magnitud de la destrucción resultante y comencé a comprender el trabajo de la base de datos, porque... Nunca antes había tratado con ellos. Solo más tarde me di cuenta de cuánto código funcional se eliminó y cuántos errores se agregaron en su lugar.

Septiembre

Al principio pensé que sería útil dominar Monga y hacerlo bien. Luego, poco a poco comencé a comprender que organizar la comunicación con la base de datos también es un arte en el que puedes hacer muchas carreras y simplemente cometer errores. Por ejemplo, si el usuario recibe dos mensajes como /subscribe - y en respuesta a cada uno crearemos una entrada en la tabla, porque al momento de procesar esos mensajes el usuario no está suscrito. Tengo la sospecha de que la comunicación con Monga en su forma actual no está escrita de la mejor manera. Por ejemplo, la configuración del usuario se creó en el momento en que se registró. Si intentó cambiarlos antes de la suscripción... el robot no respondió nada, porque el código en el actor fue a la base de datos para la configuración, no lo encontró y falló. Cuando me preguntaron por qué no crear las configuraciones necesarias, aprendí que no es necesario cambiarlas si el usuario no se ha suscrito... El sistema de filtrado de mensajes se hizo de alguna manera no obvia, e incluso después de mirar de cerca el código pude No entiendo si estaba previsto de esta manera inicialmente o si hay un error allí.

No hubo una lista de artículos enviados al chat; en cambio, me sugirieron que los escribiera yo mismo. Esto me sorprendió: en general, no estaba en contra de arrastrar todo tipo de cosas al proyecto, pero sería lógico para quien trajo estas cosas y las atornilló. Pero no, el segundo participante pareció darse por vencido en todo, pero dijo que la lista dentro del chat supuestamente era una mala solución, y que era necesario hacer un cartel con eventos como “un artículo y fue enviado al usuario x”. Luego, si el usuario solicitaba enviar nuevos artículos, era necesario enviar una solicitud a la base de datos, que seleccionaría eventos relacionados con el usuario de los eventos, también obtendría una lista de nuevos artículos, los filtraría y los enviaría al usuario. y arrojar eventos sobre esto nuevamente a la base de datos.

El segundo participante se dejó llevar por las abstracciones, cuando el bot no solo recibirá artículos de Habr y no solo será enviado a Telegram.

De alguna manera implementé eventos en forma de un cartel separado para la segunda quincena de septiembre. No es óptimo, pero al menos el bot comenzó a funcionar y comenzó a enviarme artículos nuevamente, y poco a poco descubrí lo que estaba sucediendo en el código.

Ahora puedes volver al principio y recordar que el repositorio no lo creé originalmente yo. ¿Qué pudo haber sido así? Mi solicitud de extracción fue rechazada. Resultó que tenía un código paleto, que no sabía cómo trabajar en equipo y que tenía que corregir errores en la curva de implementación actual y no refinarlo a un estado utilizable.

Me enojé y miré el historial de confirmaciones y la cantidad de código escrito. Miré momentos que originalmente estaban bien escritos y luego fueron descompuestos...

A la mierda

Me acordé del artículo tu no eres google.

Pensé que nadie realmente necesita una idea sin implementarla. Pensé que quería tener un bot que funcione, que funcione en una sola copia en una sola computadora como un simple programa Java. Sé que mi bot funcionará durante meses sin reiniciarse, ya que ya escribí dichos bots en el pasado. Si de repente cae y no envía otro artículo al usuario, el cielo no caerá al suelo y no sucederá nada catastrófico.

¿Por qué necesito Docker, mongoDB y otros programas de culto de software "serio" si el código simplemente no funciona o funciona torcidamente?

Bifurqué el proyecto e hice todo lo que quería.

Bot de Telegram para una selección personalizada de artículos de Habr

Casi al mismo tiempo, cambié de trabajo y el tiempo libre empezó a faltar muchísimo. Por la mañana me desperté en el tren, por la noche regresé tarde y ya no quería hacer nada. No hice nada por un tiempo, luego el deseo de terminar el bot me dominó y comencé a reescribir lentamente el código mientras conducía al trabajo por la mañana. No diré que fue productivo: sentarse en un tren que tiembla con una computadora portátil en su regazo y mirar el desbordamiento de la pila desde su teléfono no es muy conveniente. Sin embargo, el tiempo dedicado a escribir el código pasó completamente desapercibido y el proyecto comenzó a avanzar lentamente hacia un estado de funcionamiento.

En algún lugar en el fondo de mi mente había un gusano de duda que quería usar mongoDB, pero pensé que además de las ventajas del almacenamiento estatal "confiable", había desventajas notables:

  • La base de datos se convierte en otro punto de falla.
  • El código es cada vez más complejo y me llevará más tiempo escribirlo.
  • El código se vuelve lento e ineficiente; en lugar de cambiar un objeto en la memoria, los cambios se envían a la base de datos y, si es necesario, se retiran.
  • Existen restricciones sobre el tipo de almacenamiento de eventos en una tabla separada, que están asociadas con las peculiaridades de la base de datos.
  • La versión de prueba de Monga tiene algunas limitaciones y, si las encuentras, tendrás que iniciar y configurar Monga en algo.

Corté el monga, ahora el estado del bot simplemente se almacena en la memoria del programa y de vez en cuando se guarda en un archivo en forma de json. Quizás en los comentarios escriban que me equivoco, que aquí es donde se debe utilizar la base de datos, etc. Pero este es mi proyecto, el enfoque con el archivo es lo más sencillo posible y funciona de forma transparente.

Descartó valores mágicos como -1 y devolvió los normales. Option, almacenamiento agregado de una tabla hash con artículos enviados al objeto con información de chat. Se agregó eliminación de información sobre artículos con más de cinco días de antigüedad, para no almacenar todo. Llevé el registro a un estado de funcionamiento: los registros se escriben en cantidades razonables tanto en el archivo como en la consola. Se agregaron varios comandos de administración, como guardar el estado u obtener estadísticas como la cantidad de usuarios y artículos.

Se corrigieron un montón de pequeñas cosas: por ejemplo, para los artículos ahora se indica el número de vistas, me gusta, no me gusta y comentarios al momento de pasar el filtro del usuario. En general, es sorprendente cuántas pequeñas cosas hubo que corregir. Hice una lista, anoté todas las “irregularidades” que había y las corregí en la medida de lo posible.

Por ejemplo, agregué la capacidad de configurar todas las configuraciones directamente en un mensaje:

/subscribe
/rating +20
/author a -30
/author s -20
/author p +9000
/tag scala 20
/tag akka 50

y otro equipo /settings los muestra exactamente en este formulario, puede tomar el texto y enviar todas las configuraciones a un amigo.
Parece poca cosa, pero hay decenas de matices similares.

Se implementó el filtrado de artículos en forma de un modelo lineal simple: el usuario puede establecer una calificación adicional para autores y etiquetas, así como un valor umbral. Si la suma de la calificación del autor, la calificación promedio de las etiquetas y la calificación real del artículo es mayor que el valor umbral, entonces el artículo se muestra al usuario. Puede solicitarle artículos al bot con el comando /new o suscribirse al bot y le enviará artículos en un mensaje personal en cualquier momento del día.

En términos generales, tenía una idea para que cada artículo sacara más características (centros, cantidad de comentarios, marcadores, dinámica de cambios de calificación, cantidad de texto, imágenes y código en el artículo, palabras clave) y mostrarle al usuario un ok/ No estaba bien votar en cada artículo y entrenar un modelo para cada usuario, pero era demasiado vago.

Además, la lógica del trabajo no será tan obvia. Ahora puedo establecer manualmente una calificación de +9000 para pacienteZero y con una calificación umbral de +20 tendré la garantía de recibir todos sus artículos (a menos, por supuesto, que establezca -100500 para algunas etiquetas).

La arquitectura final resultó ser bastante simple:

  1. Un actor que almacena el estado de todos los chats y artículos. Carga su estado desde un archivo en el disco y lo guarda de vez en cuando, cada vez en un archivo nuevo.
  2. Un actor que visita la fuente RSS de vez en cuando, aprende sobre nuevos artículos, mira los enlaces, analiza y envía estos artículos al primer actor. Además, en ocasiones solicita una lista de artículos del primer actor, selecciona aquellos que no tienen más de tres días, pero que no han sido actualizados durante mucho tiempo, y los actualiza.
  3. Un actor que se comunica con un telegrama. Todavía traje el mensaje analizando completamente aquí. De manera amigable, me gustaría dividirlo en dos: uno analiza los mensajes entrantes y el segundo se ocupa de los problemas de transporte, como el reenvío de mensajes no enviados. Ahora no hay reenvío y un mensaje que no llegó debido a un error simplemente se perderá (a menos que así se indique en los registros), pero hasta ahora esto no ha causado ningún problema. Quizás surjan problemas si muchas personas se suscriben al bot y llego al límite de envío de mensajes).

Lo que me gustó es que gracias a akka las caídas de los actores 2 y 3 generalmente no afectan el desempeño del bot. Quizás algunos artículos no se actualizan a tiempo o algunos mensajes no llegan a telegram, pero la cuenta reinicia el actor y todo sigue funcionando. Guardo la información de que el artículo se muestra al usuario solo cuando el actor de Telegram responde que ha entregado el mensaje con éxito. Lo peor que me amenaza es enviar el mensaje varias veces (si se entrega, pero de alguna manera se pierde la confirmación). En principio, si el primer actor no almacenara el estado dentro de sí mismo, sino que se comunicara con alguna base de datos, entonces también podría caer imperceptiblemente y volver a la vida. También podría probar la persistencia de akka para restaurar el estado de los actores, pero la implementación actual me conviene por su simplicidad. No es que mi código fallara a menudo; al contrario, puse mucho esfuerzo en hacerlo imposible. Pero la mierda sucede, y la posibilidad de dividir el programa en partes aisladas de actores me pareció realmente conveniente y práctica.

Agregué círculo-ci para que, si el código se rompe, lo sepas inmediatamente. Como mínimo, significa que el código ha dejado de compilarse. Inicialmente quería agregar a Travis, pero solo mostraba mis proyectos sin bifurcaciones. En general, ambas cosas se pueden utilizar libremente en repositorios abiertos.

resultados

Ya es noviembre. El bot está escrito, lo he estado usando durante las últimas dos semanas y me gustó. Si tienes ideas para mejorar, escribe. No veo el sentido de monetizarlo: déjalo funcionar y envía artículos interesantes.

Enlace del robot: https://t.me/HabraFilterBot
Github: https://github.com/Kright/habrahabr_reader

Pequeñas conclusiones:

  • Incluso un proyecto pequeño puede llevar mucho tiempo.
  • No eres Google. No tiene sentido disparar a los gorriones con un cañón. Una solución sencilla puede funcionar igual de bien.
  • Los proyectos favoritos son muy buenos para experimentar con nuevas tecnologías.
  • Los bots de Telegram están escritos de forma bastante sencilla. Si no fuera por el “trabajo en equipo” y los experimentos con tecnología, el bot se habría escrito en una semana o dos.
  • El modelo de actor es algo interesante que va bien con código multiproceso y tolerante a fallos.
  • Creo que entendí por qué a la comunidad de código abierto le encantan las bifurcaciones.
  • Las bases de datos son buenas porque el estado de la aplicación ya no depende de los bloqueos o reinicios de la aplicación, pero trabajar con una base de datos complica el código e impone restricciones a la estructura de datos.

Fuente: habr.com

Añadir un comentario