commit iniziale

This commit is contained in:
diorama 2021-03-03 23:50:17 +01:00
commit 20a5434508
4 changed files with 315 additions and 0 deletions

14
LICENSE.txt Normal file
View file

@ -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
README.md Normal file
View file

@ -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
nopastspam.py Normal file
View file

@ -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
requirements.txt Normal file
View file

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