nopastspam.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. #!/usr/bin/python3
  2. # inizio
  3. print("Inizio...\n")
  4. def exitp():
  5. # funzione uscita
  6. print("\nPasso e chiudo...\n")
  7. exit()
  8. ################################################################################
  9. # moduli
  10. ################################################################################
  11. from os.path import dirname, realpath, isfile, sep
  12. import json
  13. from mastodon import Mastodon
  14. from mastodon.Mastodon import MastodonAPIError
  15. from datetime import datetime
  16. import dateutil.parser
  17. from lxml import html
  18. ################################################################################
  19. # modalità
  20. ################################################################################
  21. user_flag = True
  22. #user_flag = False # togliere cancelletto inizio riga per versione admin
  23. test_flag = True
  24. #test_flag = False # togliere cancelletto inizio riga per versione operativa (solo admin)
  25. if user_flag:
  26. print("Versione utente! Solo versione di test disponibile...\n")
  27. test_flag = True
  28. else:
  29. print("Versione admin! Maneggiare con cura se non è una versione di test...\n")
  30. if test_flag:
  31. print("Versione di TEST!\n")
  32. else:
  33. print("Versione operativa!\n")
  34. checkline = "Sì, so quel che faccio."
  35. try:
  36. rawline = input("Continuare?... Scrivere \"" + checkline + "\"\n")
  37. except EOFError:
  38. exitp()
  39. if rawline != checkline:
  40. print("Non vuoi continuare. Va bene!")
  41. # esci
  42. exitp()
  43. ################################################################################
  44. # ausiliari
  45. ################################################################################
  46. appname = "mannaggiapp"
  47. baseurl = 'https://mastodon.domain.tld'
  48. clientcredfname = 'clientcred.secret'
  49. usercredfname = 'usercred.secret'
  50. scriptdir = dirname(realpath(__file__))
  51. usercredfpath = scriptdir + sep + usercredfname
  52. adminmail = 'admin@mail.tld'
  53. adminpz = 'passwordveramentebuona'
  54. default_following_count = 7
  55. avatar_missing = baseurl + "/avatars/original/missing.png"
  56. header_missing = baseurl + "/headers/original/missing.png"
  57. def find_anchor(element):
  58. if element.text:
  59. anchor = element.find("a")
  60. if anchor is not None:
  61. return True
  62. return False
  63. ################################################################################
  64. # registro app e token
  65. ################################################################################
  66. print("Registro app e token, oppure uso token...")
  67. if not isfile(usercredfpath):
  68. ############################################################################
  69. # token non registrato
  70. ############################################################################
  71. print(" Creo app e token...")
  72. Mastodon.create_app(
  73. appname,
  74. api_base_url = baseurl,
  75. to_file = clientcredfname
  76. )
  77. mastodon = Mastodon(
  78. client_id = clientcredfname,
  79. api_base_url = baseurl
  80. )
  81. mastodon.log_in(
  82. adminmail,
  83. adminpz,
  84. to_file = usercredfname
  85. )
  86. 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))
  87. print("Rilancia lo script dopo aver tolto la password.\n")
  88. exitp()
  89. else:
  90. ############################################################################
  91. # token registrato
  92. ############################################################################
  93. print(" Uso token...")
  94. if adminpz:
  95. 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")
  96. mastodon = Mastodon(
  97. access_token = usercredfname,
  98. api_base_url = baseurl
  99. )
  100. ################################################################################
  101. # ottenimento degli accounts
  102. ################################################################################
  103. if test_flag:
  104. ############################################################################
  105. # ottenimento di account di prova
  106. ############################################################################
  107. if isfile("accounts-json.txt"):
  108. ########################################################################
  109. # ottenimento di account da file json
  110. ########################################################################
  111. print("Ottieni accounts salvati su file json...")
  112. with open("accounts-json.txt", "r") as f:
  113. lines = f.read()
  114. accounts = json.loads(lines)
  115. else:
  116. ########################################################################
  117. # ottenimento di account da istanza
  118. ########################################################################
  119. print("Ottieni accounts nella directory dei profili pubblica...")
  120. print(" Seleziona accounts nella directory dei profili (n.b. id salvati su file)")
  121. with open("directory-profili.txt","r") as f:
  122. lines = f.read()
  123. splits = lines.split()
  124. accounts = []
  125. print(" Ottieni ogni account tramite la API di Mastodon")
  126. 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.")
  127. nsplits = len(splits)
  128. fsplits = len(str(nsplits))
  129. for isplit, split in enumerate(splits):
  130. fstr = " {:" + str(fsplits) +"d}/{} {}"
  131. print(fstr.format(isplit+1, nsplits, split))
  132. try:
  133. account = mastodon.account(split)
  134. accounts.append(account)
  135. except MastodonAPIError as e:
  136. pass # account non trovato (sospeso oppure altro)
  137. if isplit == 271:
  138. break
  139. with open("accounts-json.txt", "w") as f:
  140. json_str = json.dumps(accounts, default=str)
  141. f.write(json_str)
  142. print(" Accounts scritti in formato JSON")
  143. else:
  144. ############################################################################
  145. # ottenimento di tutti gli account di istanza
  146. ############################################################################
  147. print("Ottieni tutti gli accounts di istanza...")
  148. try:
  149. accounts = mastodon.admin_accounts() # tutti gli account locali
  150. except MastodonAPIError:
  151. print("L'account \"\" non ha l'autorizzazione necessaria per ottenere tutti gli account di istanza")
  152. exitp()
  153. print("Accounts ottenuti!\n")
  154. ################################################################################
  155. # selezione (+ sospensione se non è un test) accounts
  156. ################################################################################
  157. for account in accounts:
  158. ############################################################################
  159. # aggiusta interfaccia
  160. ############################################################################
  161. # stessa interfaccia admin account dict e user account dict
  162. if "account" in account:
  163. # admin account dict that wraps account dict
  164. # https://mastodonpy.readthedocs.io/en/stable/#admin-account-dicts
  165. account = account["account"]
  166. date_ = account["created_at"]
  167. # controllo su data
  168. if type(date_) == datetime:
  169. date = date_
  170. elif type(date_) == str:
  171. date = dateutil.parser.parse(date_)
  172. else:
  173. raise TypeError(
  174. "{} è di tipo {} e non è di un tipo appropriato (stringa o datetime)".format(date, type(date))
  175. )
  176. ############################################################################
  177. # condizioni su account
  178. ############################################################################
  179. # casi 0: quando non sospendere
  180. # salta: se l'account è creato dopo fine giugno 2020
  181. if date > datetime(2020, 6, 30, 23, 59, 59).astimezone(): # luglio o poi
  182. continue
  183. # salta: se l'account ha almeno uno status
  184. if account["statuses_count"] > 0: # almeno uno status
  185. continue
  186. # variabili di servizio
  187. note = html.fromstring(account["note"]) # biografia del profilo
  188. fields = account["fields"] # metadati del profilo
  189. avatar = account["avatar"]
  190. header = account["header"]
  191. ############################################################################
  192. # casi 1+: quando sospendere
  193. # caso 1: account "vuoto"
  194. # zero metadati, biografia vuota, zero toot, account vecchio
  195. if note.text is None and len(fields) == 0: # biografia vuota e zero metadati
  196. if account["following_count"] <= default_following_count:
  197. continue # salta: si assume no spam tramite seguiti che vanno in federata)
  198. #TODO: check contro i seguiti di default (n.b. questi cambiano di volta in volta)
  199. if avatar != avatar_missing or header != header_missing:
  200. continue # salta: account personalizzato in profilo o intestazione
  201. #nei casi restanti: sospendi
  202. print(account["url"] + " caso 1") # da sospendere
  203. if not test_flag:
  204. mastodon.admin_account_moderate(account["id"], action="suspend", send_email_notification=False)
  205. continue # salta loop sempre: altri casi sono esclusi (anche in caso di test)
  206. ############################################################################
  207. # caso 2: account spam
  208. # link nei metadati o biografia, zero toot, account vecchio
  209. #TODO: check email, numeri di telefono
  210. # link nella biografia
  211. if find_anchor(note):
  212. print(account["url"] + " caso 2a") # da sospendere
  213. if not test_flag:
  214. mastodon.admin_account_moderate(account["id"], action="suspend", send_email_notification=False)
  215. continue # possono esserci altri valori di metadati con ancore (vs. caso 1)
  216. # link nei metadati
  217. for field in fields:
  218. namestr = field["name"]
  219. if namestr:
  220. name = html.fromstring(namestr)
  221. # link
  222. if find_anchor(name):
  223. print(account["url"] + " caso 2a1") # da sospendere
  224. if not test_flag:
  225. mastodon.admin_account_moderate(account["id"], action="suspend", send_email_notification=False)
  226. continue # possono esserci altri nomi di metadati con ancore (vs. caso 1)
  227. valuestr = field["value"]
  228. if valuestr:
  229. value = html.fromstring(valuestr)
  230. if find_anchor(value):
  231. print(account["url"] + " caso 2a2") # da sospendere
  232. if not test_flag:
  233. mastodon.admin_account_moderate(account["id"], action="suspend", send_email_notification=False)
  234. continue # possono esserci altri valori di metadati con ancore (vs. caso 1)
  235. ############################################################################
  236. # fine
  237. exitp()