commit iniziale
This commit is contained in:
commit
20a5434508
4 changed files with 315 additions and 0 deletions
14
LICENSE.txt
Normal file
14
LICENSE.txt
Normal 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
47
README.md
Normal 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
252
nopastspam.py
Normal 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
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Mastodon.py==1.5.1
|
||||||
|
lxml==4.6.2
|
Loading…
Reference in a new issue