Browse Source

commit iniziale

diorama 3 years ago
commit
20a5434508
4 changed files with 315 additions and 0 deletions
  1. 14 0
      LICENSE.txt
  2. 47 0
      README.md
  3. 252 0
      nopastspam.py
  4. 2 0
      requirements.txt

+ 14 - 0
LICENSE.txt

@@ -0,0 +1,14 @@
+0BSD
+
+No rights reserved 2021 diorama
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.

+ 47 - 0
README.md

@@ -0,0 +1,47 @@
+# A che serve?
+_nopastspam.py_ è uno script che serve a sospendere account [Mastodon](https://mastodon.help/) a zero toot, considerati spam o inattivi, e creati prima di una certa data. Lo script si basa su un [wrapper python](https://github.com/halcy/Mastodon.py) della [API di Mastodon](https://docs.joinmastodon.org/api/).
+
+## Come si installa?
+`cd [QUESTA_CARTELLA]`  
+`pip3 install -r requirements.txt`
+
+## Come si usa?
+Prima di tutto bisogna cambiare i parametri dello script:  
+1. `baseurl = 'https://mastodon.domain.tld'`  
+2. `adminmail = 'admin@mail.tld'`  
+3. `adminpz = 'passwordveramentebuona'`  
+inserendo fra gli apicini dopo il segno di uguale l'indirizzo dell'istanza, la mail di un account e la password, rispettivamente per ogni linea.  
+Fatto questo, lo script va lanciato con `python3` (testato per `python3.6`).
+`python3 [QUESTO_SCRIPT]`
+Va lanciato una prima volta per registrare lo script sul server. Le volte successive a seconda per lanciarlo effettivamente.  
+Dopo il primo lancio, basta lanciarlo solo una volta per ogni volta che si vuol
+
+
+## Quali versioni esistono?
+Per la versione admin di test, serve un account admin e va tolto il cancelletto davanti la riga che inizia per `#user_flag`. In questo modo si ottengono tutti gli account dell'istanza e si selezionano quelli rilevanti.  
+Per la versione admin operativa, serve un account admin e va tolto il cancelletto sia davanti la riga che inizia per `#user_flag`, sia davanti la riga che inizia per `#test_flag`.
+La versione admin di test è consigliata rispetto alla versione utente di test perché la prima fa una sola richiesta tramite la API di Mastodon mentre la seconda ne fa troppe e [ha un limite](https://docs.joinmastodon.org/api/rate-limits/). Si consiglia di lanciare almeno una volta la versione admin di test e di testare per sé la versione admin operativa. _N.B.: la sospensione degli account non è stata testata._
+
+## Quali account sono considerati spam o inattivi?
+La versione di default seleziona tutti quegli account creati prima di luglio 2020 (data di default), con zero toot e che soddisfano uno dei due criteri seguenti:
+a- account inattivi a zero toot: non hanno metadati, biografia, profilo, intestazione, e hanno un numero di follow maggiore del numero di account in autofollow (numero di default: 7);
+b- account spam a zero toot: hanno un link nei metadati o nella biografia.
+
+### Ma sono criteri discrezionali!
+Certo. Come tutti i criteri.
+
+## Ci sono delle avvertenze?
+Sul caso a: non sono selezionati gli account spam a zero toot con meno seguaci del numero predefinito di autofollow.  
+Sul caso b: non si fa differenza tra link fuori policy o link dentro policy. Un link a un'iniziativa antirepressione sta al livello di un casinò online. (falsi positivi)
+
+### Come si risolve follow spambot?
+Col numero di soglia per l'autofollow non è possibile selezionare gli spambot che rimuovono tutti i follow e ne mettono pochi in altre istanze. Questa è una strategia comune per finire in timeline federata. Il criterio di autofollow è inteso a tutela dee lurker. Altri criteri sono possibili, ad es. raffrontare i follow dell'account con gli autofollow attuali o tutti quelli che ci sono stati nel tempo. Se gli spambot con follow ad altre istanze vi creano particolari problemi, al momento potete cambiare direttamente il codice o forkare.
+
+## Licenza?
+0BSD.
+
+## Garanzie?
+Mai date. Usare lo script a proprio rischio e pericolo.
+
+## Mano?
+Mail qui: _mionomeutente a riseup punto net_. Più facile che legga per mail che altrove.

+ 252 - 0
nopastspam.py

@@ -0,0 +1,252 @@
+#!/usr/bin/python3
+
+# inizio
+print("Inizio...\n")
+
+def exitp():
+    # funzione uscita
+    print("\nPasso e chiudo...\n")
+    exit()
+
+################################################################################
+# moduli
+################################################################################
+from os.path import dirname, realpath, isfile, sep
+import json
+from mastodon import Mastodon
+from mastodon.Mastodon import MastodonAPIError
+from datetime import datetime
+import dateutil.parser
+from lxml import html
+
+################################################################################
+# modalità
+################################################################################
+user_flag = True
+#user_flag = False # togliere cancelletto inizio riga per versione admin
+test_flag = True
+#test_flag = False # togliere cancelletto inizio riga per versione operativa (solo admin)
+
+if user_flag:
+    print("Versione utente! Solo versione di test disponibile...\n")
+    test_flag = True
+else:
+    print("Versione admin! Maneggiare con cura se non è una versione di test...\n")
+
+if test_flag:
+    print("Versione di TEST!\n")
+else:
+    print("Versione operativa!\n")
+    checkline = "Sì, so quel che faccio."
+    try:
+        rawline = input("Continuare?... Scrivere \"" + checkline + "\"\n")
+    except EOFError:
+        exitp()
+    if rawline != checkline:
+        print("Non vuoi continuare. Va bene!")
+        # esci
+        exitp()
+
+################################################################################
+# ausiliari
+################################################################################
+appname = "mannaggiapp"
+baseurl = 'https://mastodon.domain.tld'
+clientcredfname = 'clientcred.secret'
+usercredfname   = 'usercred.secret'
+scriptdir = dirname(realpath(__file__))
+usercredfpath = scriptdir + sep + usercredfname 
+
+adminmail = 'admin@mail.tld'
+adminpz = 'passwordveramentebuona'
+
+default_following_count = 7
+avatar_missing = baseurl + "/avatars/original/missing.png"
+header_missing = baseurl + "/headers/original/missing.png"
+
+def find_anchor(element):
+    if element.text:
+        anchor = element.find("a")
+        if anchor is not None:
+            return True
+    return False
+
+################################################################################
+# registro app e token
+################################################################################
+print("Registro app e token, oppure uso token...")
+if not isfile(usercredfpath):
+    ############################################################################
+    # token non registrato
+    ############################################################################
+    print("    Creo app e token...")
+    Mastodon.create_app(
+        appname,
+        api_base_url = baseurl,
+        to_file = clientcredfname
+    )
+    mastodon = Mastodon(
+        client_id = clientcredfname,
+        api_base_url = baseurl
+    )
+    mastodon.log_in(
+        adminmail,
+        adminpz,
+        to_file = usercredfname
+    )
+    print("Buone notizie! La app \"{}\" è stata registrata!\nOra preoccupati di ***TOGLIERE*** la password dallo script!\nVa bene cambiare il valore di \'adminpz\' con qualsiasi altro valore valido!\nAnche una stringa vuota: letteralmente \"\"".format(appname))
+    print("Rilancia lo script dopo aver tolto la password.\n")
+    exitp()
+else:
+    ############################################################################
+    # token registrato
+    ############################################################################
+    print("    Uso token...")
+    if adminpz:
+        print("Avviso! Hai tolto la password dallo script?\nSe no, ***TOGLILA***: va bene cambiare il valore di \'adminpz\' con qualsiasi altro valore valido!\nAnche una stringa vuota: letteralmente \"\"\n")
+    mastodon = Mastodon(
+        access_token = usercredfname,
+        api_base_url = baseurl
+    )
+
+################################################################################
+# ottenimento degli accounts
+################################################################################
+if test_flag:
+    ############################################################################
+    # ottenimento di account di prova
+    ############################################################################
+    if isfile("accounts-json.txt"):
+        ########################################################################
+        # ottenimento di account da file json
+        ########################################################################
+        print("Ottieni accounts salvati su file json...")
+        with open("accounts-json.txt", "r") as f:
+            lines = f.read()
+            accounts = json.loads(lines)
+    else:
+        ########################################################################
+        # ottenimento di account da istanza
+        ########################################################################
+        print("Ottieni accounts nella directory dei profili pubblica...")
+        print("    Seleziona accounts nella directory dei profili (n.b. id salvati su file)")
+        with open("directory-profili.txt","r") as f:
+            lines = f.read()
+        splits = lines.split()
+        accounts = []
+        print("    Ottieni ogni account tramite la API di Mastodon")
+        print("    N.B.: default di 300 richieste ogni 5 minuti... per 12k accounts sono 3+ ore! Usare interfaccia admin.\nLimite di 270 qui per test.")
+        nsplits = len(splits)
+        fsplits = len(str(nsplits))
+        for isplit, split in enumerate(splits):
+            fstr = "    {:" + str(fsplits) +"d}/{} {}"
+            print(fstr.format(isplit+1, nsplits, split))
+            try:
+                account = mastodon.account(split)
+                accounts.append(account)
+            except MastodonAPIError as e:
+                pass # account non trovato (sospeso oppure altro)
+            if isplit == 271:
+                break
+        with open("accounts-json.txt", "w") as f:
+            json_str = json.dumps(accounts, default=str)
+            f.write(json_str)
+        print("    Accounts scritti in formato JSON")
+else:
+    ############################################################################
+    # ottenimento di tutti gli account di istanza
+    ############################################################################
+    print("Ottieni tutti gli accounts di istanza...")
+    try:
+        accounts = mastodon.admin_accounts() # tutti gli account locali
+    except MastodonAPIError:
+        print("L'account \"\" non ha l'autorizzazione necessaria per ottenere tutti gli account di istanza")
+        exitp()
+print("Accounts ottenuti!\n")
+
+################################################################################
+# selezione (+ sospensione se non è un test) accounts
+################################################################################
+for account in accounts:
+    ############################################################################
+    # aggiusta interfaccia
+    ############################################################################
+    # stessa interfaccia admin account dict e user account dict
+    if "account" in account:
+        # admin account dict that wraps account dict
+        # https://mastodonpy.readthedocs.io/en/stable/#admin-account-dicts
+        account = account["account"]
+    date_ = account["created_at"]
+    # controllo su data
+    if type(date_) == datetime:
+        date = date_
+    elif type(date_) == str:
+        date = dateutil.parser.parse(date_)
+    else:
+        raise TypeError(
+            "{} è di tipo {} e non è di un tipo appropriato (stringa o datetime)".format(date, type(date))
+        )
+    ############################################################################
+    # condizioni su account
+    ############################################################################
+    # casi 0: quando non sospendere
+    # salta: se l'account è creato dopo fine giugno 2020
+    if date > datetime(2020, 6, 30, 23, 59, 59).astimezone(): # luglio o poi
+        continue
+    # salta: se l'account ha almeno uno status
+    if account["statuses_count"] > 0: # almeno uno status
+        continue
+    # variabili di servizio
+    note = html.fromstring(account["note"]) # biografia del profilo
+    fields = account["fields"] # metadati del profilo
+    avatar = account["avatar"]
+    header = account["header"]
+    ############################################################################
+    # casi 1+: quando sospendere
+    # caso 1: account "vuoto"
+    # zero metadati, biografia vuota, zero toot, account vecchio
+    if note.text is None and len(fields) == 0: # biografia vuota e zero metadati
+        if account["following_count"] <= default_following_count:
+            continue # salta: si assume no spam tramite seguiti che vanno in federata)
+        #TODO: check contro i seguiti di default (n.b. questi cambiano di volta in volta)
+        if avatar != avatar_missing or header != header_missing:
+            continue # salta: account personalizzato in profilo o intestazione
+        #nei casi restanti: sospendi
+        print(account["url"] + " caso 1") # da sospendere
+        if not test_flag:
+            mastodon.admin_account_moderate(account["id"], action="suspend", send_email_notification=False)
+        continue # salta loop sempre: altri casi sono esclusi (anche in caso di test)
+    ############################################################################
+    # caso 2: account spam
+    # link nei metadati o biografia, zero toot, account vecchio
+    #TODO: check email, numeri di telefono
+    # link nella biografia
+    if find_anchor(note):
+        print(account["url"] + " caso 2a") # da sospendere
+        if not test_flag:
+            mastodon.admin_account_moderate(account["id"], action="suspend", send_email_notification=False)
+            continue # possono esserci altri valori di metadati con ancore (vs. caso 1)
+    # link nei metadati
+    for field in fields:
+        namestr = field["name"]
+        if namestr:
+            name = html.fromstring(namestr)
+            # link 
+            if find_anchor(name):
+                print(account["url"] + " caso 2a1") # da sospendere
+                if not test_flag:
+                    mastodon.admin_account_moderate(account["id"], action="suspend", send_email_notification=False)
+                    continue # possono esserci altri nomi di metadati con ancore (vs. caso 1)
+        valuestr = field["value"]
+        if valuestr:
+            value = html.fromstring(valuestr)
+            if find_anchor(value):
+                print(account["url"] + " caso 2a2") # da sospendere
+                if not test_flag:
+                    mastodon.admin_account_moderate(account["id"], action="suspend", send_email_notification=False)
+                    continue # possono esserci altri valori di metadati con ancore (vs. caso 1)
+    ############################################################################
+
+# fine
+exitp()
+

+ 2 - 0
requirements.txt

@@ -0,0 +1,2 @@
+Mastodon.py==1.5.1
+lxml==4.6.2