10 de noviembre de 2024
Creación de una simple aplicación que interactua con mastodon

Aquí volvemos otra vez para añadir un poco de vidilla a ésto de hacer cositas con python.

En este caso vengo con una versión lite de una automatización que estoy haciendo en mi raspberry pi donde tengo muchísismos procesos ejecutándose todo el día.

Una mini aplicación que funciona con mastodon con el que se pueden hacer verdaderas virguerías.

La idea de ésta aplicación será conectarse a un servidor de mastodon (con usuario y contraseña), comprobar el número de posts que se han publicado y si superan un máximo que nosotros le hayamos indicado borre los más viejos hasta que se quede el número máximo.

También comprobará todas las notificaciones (interacciones con los posts de otros usuarios) y a cada usuario que haya interactuado con algúno de los posts comprobará si lo sigue y le seguirá automáticamente.

Lo primero será instalar la librería de mastodon.

pip install Mastodon.py

Una vez instalado tendremos que tener en cuenta que el código está adaptado para mi equipo por lo que habría que crear en %appdata% una serie de carpetas y añadir ahí un icono para que se vea en el area de notificación. Lo llamaremos mastodon.ico y así cuando se ejecute lo veríamos

De esta manera podremos parar la ejecución fácilmente 🙂

Y aquí dejo el código con algunas anotaciones que he añadido para hacerlo entendible. Espero que os sea de utilidad.

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
#Añadimos donde están el repositorio de código para cuando se ejecute en linux (en mi caso es necesario por la configuracion de la raspberry pi)
sys.path.append("/home/pi/python/source/") 
import os
import platform
import time
import re
from threading import Thread
from tkinter import *
from pystray import MenuItem as item
import pystray
from PIL import Image
import shutil
import requests
from mastodon import Mastodon
from os.path import exists 

"""---------------------------------------------------------------------------------------------
Funcion asincrona para la pantalla grafica mientras se ejecuta el bot
---------------------------------------------------------------------------------------------"""
class AsyncApp(Thread):
    def __init__(self, pathIcon):
        super().__init__()
        self.pathIcon = pathIcon
    def run(self):
        myApp(pathIcon)

"""---------------------------------------------------------------------------------------------
Clase de la aplicacion grafica
---------------------------------------------------------------------------------------------"""
class myApp:
    #Crearemos una aplicación simple que lo único que haga será mostar un icono en el area de notificaciones y si pulsamos boton derecho saldrá
    #un menú con una única opción que será Salir que parará toda la ejecución
    def __init__(self, pathIcon):
        self.win=Tk()
        self.win.title("Mastodon APP")
        self.win.geometry("700x350")       
        self.win.withdraw()
        image=Image.open(pathIcon)
        menu=(
            item('Salir', self.quit_window),
            )
        icon=pystray.Icon("name", image, "Mastodon APP", menu)
        icon.run()
        self.win.mainloop()

    def quit_window(self, icon, item):
        icon.stop()
        self.win.destroy()
        os._exit(0)
        sys.exit() #Este está puesto por si acaso no funcionase el exit

"""-------------------------------------------------------------------------------------------------------------------
CLASE DE MASTODON PARA INTERACTUAR
"-------------------------------------------------------------------------------------------------------------------"""
class mastodonUtil: 

    def __init__(self, server, account, password, mail): 

        if platform.system() == "Windows":
            #Si se está ejecutando en windows obtiene la ruta en la que se guardará el fichero secret con la sesión de mastodon
            secretPath = f"{os.environ['APPDATA']}/automatics/mastodon/data"
        else:
            #En caso de que sea linux el entorno se pone la ruta en la que se guardará el fichero
            secretPath = "/home/pi/python/configs"
        if not os.path.exists(secretPath):
            os.makedirs(secretPath)
        secret = secretPath + "/{0}_{1}.secret".format(str(server).replace("https://","").replace("http://","").replace(".",""), account) 
        file_exists = exists(secret) 

        #pytooterapp 
        if file_exists == False: 
            Mastodon.create_app( 
                'SkeithAutomaticApp', #Nombre de la aplicación que se verá cuando se postee en mastodono
                api_base_url=server, 
                to_file=secret 
            ) 

            mastodon = Mastodon( 
                client_id=secret, 
                api_base_url=server 
            ) 
            mastodon.log_in( 
                mail, 
                password, 
                to_file=secret 
            ) 

        self.mastodon = Mastodon( 
            access_token=secret, 
            api_base_url=server 
        ) 
    
    #Función que obtiene las notificaciones del usuario (si han rebloqueado algo, dado a me gusta, recibido algún mensaje...etc)
    def getNotificaciones(self):
        return self.mastodon.notifications()
    
    #Función que borra un toot publicado indicándoloe el id del post (status para mastodon)
    def deleteStatus(self, idStatus):
        return self.mastodon.status_delete(id=idStatus)
        
    #Función que obtiene los datos de relación con un usuario, nos dirá si nos sigue, si le seguimos...etc
    def getStatusRelationship(self, id):
        return self.mastodon.account_relationships(id)
    
    def followAccount(self, id):
        return self.mastodon.account_follow(id)
    
    #Función que borra todas las notificaciones (no es marcarlas como leidas, es hacer que desaparezcan)
    def clearNotifications(self):
        return self.mastodon.notifications_clear()
    
    #Función que otbiene todos los posts publicados por el usuario con el que nos hemos logueado
    def getStatuses(self):
        cuenta = self.mastodon.me()
        salir = False
        maxId = None
        timeLineAll = []
        cuenta = self.mastodon.me()
        while salir == False:
            if maxId != None:
                timeLine = self.mastodon.account_statuses(id=cuenta.id, limit=100, max_id=maxId)
            else:
                timeLine = self.mastodon.account_statuses(id=cuenta.id, limit=100)
            for status in timeLine:
                if status.account.id == cuenta.id:
                    timeLineAll.append(status)
            try:
                maxId = timeLine._pagination_next['max_id']
            except:
                salir = True
        return timeLineAll
"""---------------------------------------------------------------------------------------------------------------------------
FUNCIONES PARA EL TRATAMIENTO DE FICHEROS INTERNOS
---------------------------------------------------------------------------------------------------------------------------"""
#Función que obtiene ficheros del almacenamiento interno
def getFiles(filePath, fileName):
    pathNewFile = filePath + fileName
    basefilePath = resolver_ruta(fileName)
    if os.path.isfile(pathNewFile):
        """ Ya existe el fichero así que no se hará nada"""
    else:
        shutil.copy(basefilePath, pathNewFile)
    return pathNewFile

#Obtiene la ruta del fichero interno
def resolver_ruta(ruta_relativa):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, ruta_relativa)
    return os.path.join(os.path.abspath('.'), ruta_relativa)

"""---------------------------------------------------------------------------------------------------------------------------
INICIO DEL PROGRAMA
---------------------------------------------------------------------------------------------------------------------------"""
if platform.system() == "Windows":
    #Si se está ejecutando en windows obtiene el icono de la aplicacion (que se verá en el area de notificación en %appdata%), previamente la tendremos que haber colocado ahí
    #Se puede añadir al código para que lo haga automáticamente pero para no liarlo mucho lo he quitado y digamos que si estará el icono en dicha carpeta.
    dir_path_config = f"{os.environ['APPDATA']}/automatics/mastodon/data"
    pathIcon = f"{dir_path_config}/mastodon.ico"
else:
    #En caso de que sea linux el entorno se pone la ruta en la que está el icono
    pathIcon = "/home/pi/python/imagenes/mastodon.ico"

CLEANR = re.compile('<.*?>') #Esta expresión regular la usarmos para limpiar el texto de los posts/status porque vienen en html
MAX_POSTS = 1500 #Número máximo de posts que vamos a dejar que haya publicados con la cuenta

"""---------------------------------------------------------------------------------------------------------------------------
INICIAMOS LA APLICACION EN EL ICONO DE NOTIFICACIONES
---------------------------------------------------------------------------------------------------------------------------"""
#Se inicia la aplicación en el area de notificación (de esta forma sabremos que se está ejecutando y podremos pararla desde ahi)
#La aplicación se lanzará de manera asíncrona (en hilo) y se quedará paralelamente ejecutándose
launchApp = AsyncApp(pathIcon)
launchApp.start()

while True: #Un loop que nunca estará a false por lo que se ejecutará eternamente (a no ser que mandemos desde el area de notificaciones que pare)
    """------------------------------------------------------------------------------------------------------------------
    INICIAR SESION EN EL SERVIDOR DE MASTODON
    ------------------------------------------------------------------------------------------------------------------"""
    sesion = mastodonUtil("url del servidor Mastodon", "Usuario", "Contraseña", 'Correo con el que se registró en el servidor')
    
    """------------------------------------------------------------------------------------------------------------------
    TRATAMIENTO DE LOS POSTS CUANDO HAY MAS DE MAX_POSTS CON INTERACCION QUE BORRAMOS LOS MÁS VIEJOS
    ------------------------------------------------------------------------------------------------------------------"""
    timeLine = sesion.getStatuses()
    if len(timeLine) > MAX_POSTS:
        timeLineReverse = timeLine[::-1] #Invertimos el resultado para que estén los más viejos primero para poder ir borrando siempre de antiguo a nuevo
        postsRestantes = len(timeLineReverse)
        for post in timeLineReverse:
            if postsRestantes <= MAX_POSTS:
                break   
            estadoBorrado = sesion.deleteStatus(post.id)
            contenidoStatus = re.sub(CLEANR, '', post.content) #Limpiamos el contendido con la expresión para mostarlo correctament
            print(f"Eliminado - {contenidoStatus}")
            time.sleep(60) #Esperamos un tiempo entre borrado y borrado para no saturar el servidor
            postsRestantes = postsRestantes - 1
        print(f"Fin de borrar posts: Posts antiguos cuando es más de {MAX_POSTS}: | Posts actuales {postsRestantes}")
    
    """------------------------------------------------------------------------------------------------------------------
    TRATAMIENTO DE LAS NOTIFICACIONES INSERTANDOLAS EN LA TABLA Y SE SIGUEL AL USUARIO QUE HA INTERACTUADO
    ------------------------------------------------------------------------------------------------------------------"""
    notificaciones = sesion.getNotificaciones()
    sesion.clearNotifications()
    for notificacion in notificaciones:
        #Aquí estamos sacando información de más, es un ejemplo de cosas que podemos obtener de mastodono
        idStatus = notificacion.status.id
        contentStatus = re.sub(CLEANR, '', notificacion.status.content) #Limpiamos el contendido con la expresión para mostarlo correctament
        usuarioNotificacion = notificacion.account.username
        cuentaNotificacion = notificacion.account.id
        estado = ""
        tipoNotificacion = notificacion.type
        idCuentaAccion = notificacion.account.id
        estadoRelacion = sesion.getStatusRelationship(idCuentaAccion)
        for estado in estadoRelacion:
            if estado.following == False:
                seguir = sesion.followAccount(idCuentaAccion)
                usuario = str(notificacion.account.username).replace("_"," ")
                tipo = str(notificacion.type)
                print(f"Seguimos a {usuario} porque ha ineteractuado {tipo}")
    print("Fin de autoseguir por interacciones con los posts")

    """------------------------------------------------------------------------------------------------------------------
    TIEMPO DE ESPERA ENTRE INTERACCIONES
    ------------------------------------------------------------------------------------------------------------------"""
    time.sleep(3600) #Hemos puesto un tiempo de espera para que se ejecute en loop cada 1h todo el proceso

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *