nopastspam/nopastspam.py
2021-03-03 23:50:17 +01:00

252 lines
11 KiB
Python

#!/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()