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