|
@@ -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()
|
|
|
+
|