1 # Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
2 # Copyright (C) 2023 Free Software Foundation
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published
6 # by the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 from fba import blacklist
22 from fba import config
24 from fba import instances
25 from fba import network
27 from fba.helpers import dicts
28 from fba.helpers import tidyup
30 def fetch_peers(domain: str) -> list:
31 # DEBUG: print(f"DEBUG: domain({len(domain)})={domain} - CALLED!")
32 if not isinstance(domain, str):
33 raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
35 raise ValueError("Parameter 'domain' is empty")
37 # DEBUG: print(f"DEBUG: domain='{domain}' is misskey, sending API POST request ...")
40 step = config.get("misskey_limit")
43 # DEBUG: print(f"DEBUG: Checking CSRF for domain='{domain}'")
45 headers = csrf.determine(domain, dict())
46 except network.exceptions as exception:
47 print(f"WARNING: Exception '{type(exception)}' during checking CSRF - EXIT!")
50 # iterating through all "suspended" (follow-only in its terminology)
51 # instances page-by-page, since that troonware doesn't support
52 # sending them all at once
54 # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
56 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
62 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
70 # DEBUG: print(f"DEBUG: fetched[]={type(fetched)}")
71 if "error_message" in fetched:
72 print(f"WARNING: post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
73 instances.update_last_error(domain, fetched)
75 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
76 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
77 instances.update_last_error(domain, fetched["json"]["error"]["message"])
80 rows = fetched["json"]
82 # DEBUG: print(f"DEBUG: rows()={len(rows)}")
84 # DEBUG: print(f"DEBUG: Returned zero bytes, exiting loop, domain='{domain}'")
86 elif len(rows) != config.get("misskey_limit"):
87 # DEBUG: print(f"DEBUG: Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
88 offset = offset + (config.get("misskey_limit") - len(rows))
90 # DEBUG: print(f"DEBUG: Raising offset by step={step}")
91 offset = offset + step
94 # DEBUG: print(f"DEBUG: rows({len(rows)})[]={type(rows)}")
96 # DEBUG: print(f"DEBUG: row()={len(row)}")
98 print(f"WARNING: row()={len(row)} does not contain key 'host': {row},domain='{domain}'")
100 elif not isinstance(row["host"], str):
101 print(f"WARNING: row[host][]={type(row['host'])} is not 'str'")
103 elif blacklist.is_blacklisted(row["host"]):
104 # DEBUG: print(f"DEBUG: row[host]='{row['host']}' is blacklisted. domain='{domain}'")
106 elif row["host"] in peers:
107 # DEBUG: print(f"DEBUG: Not adding row[host]='{row['host']}', already found.")
108 already = already + 1
111 # DEBUG: print(f"DEBUG: Adding peer: '{row['host']}'")
112 peers.append(row["host"])
114 if already == len(rows):
115 # DEBUG: print(f"DEBUG: Host returned same set of '{already}' instances, aborting loop!")
118 # DEBUG: print(f"DEBUG: Adding '{len(peers)}' for domain='{domain}'")
119 instances.set_data("total_peers", domain, len(peers))
121 # DEBUG: print(f"DEBUG: Updating last_instance_fetch for domain='{domain}' ...")
122 instances.update_last_instance_fetch(domain)
124 # DEBUG: print(f"DEBUG: Returning peers[]='{type(peers)}'")
127 def fetch_blocks(domain: str) -> dict:
128 # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
129 if not isinstance(domain, str):
130 raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
132 raise ValueError("Parameter 'domain' is empty")
134 # DEBUG: print(f"DEBUG: Fetching misskey blocks from domain={domain}")
141 step = config.get("misskey_limit")
144 # DEBUG: print(f"DEBUG: Checking CSRF for domain='{domain}'")
146 headers = csrf.determine(domain, dict())
147 except network.exceptions as exception:
148 print(f"WARNING: Exception '{type(exception)}' during checking CSRF - EXIT!")
151 # iterating through all "suspended" (follow-only in its terminology)
152 # instances page-by-page since it doesn't support sending them all at once
155 # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
157 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
158 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
165 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
166 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
171 "offset" : offset - 1
174 # DEBUG: print(f"DEBUG: fetched[]={type(fetched)}")
175 if "error_message" in fetched:
176 print(f"WARNING: post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
177 instances.update_last_error(domain, fetched)
179 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
180 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
181 instances.update_last_error(domain, fetched["json"]["error"]["message"])
184 rows = fetched["json"]
186 # DEBUG: print(f"DEBUG: rows({len(rows)})={rows} - suspend")
188 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
190 elif len(rows) != config.get("misskey_limit"):
191 # DEBUG: print(f"DEBUG: Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
192 offset = offset + (config.get("misskey_limit") - len(rows))
194 # DEBUG: print("DEBUG: Raising offset by step:", step)
195 offset = offset + step
198 for instance in rows:
200 # DEBUG: print(f"DEBUG: instance[{type(instance)}]='{instance}' - suspend")
201 if instance["isSuspended"] and not dicts.has_key(blocklist["suspended"], "domain", instance["host"]):
203 blocklist["suspended"].append(
205 "domain": tidyup.domain(instance["host"]),
206 # no reason field, nothing
211 # DEBUG: print(f"DEBUG: count={count}")
213 # DEBUG: print("DEBUG: API is no more returning new instances, aborting loop!")
216 except network.exceptions as exception:
217 print(f"WARNING: Caught error, exiting loop: domain='{domain}',exception[{type(exception)}]='{str(exception)}'")
218 instances.update_last_error(domain, exception)
223 # Fetch blocked (full suspended) instances
226 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
227 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
234 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
235 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
240 "offset" : offset - 1
243 # DEBUG: print(f"DEBUG: fetched[]={type(fetched)}")
244 if "error_message" in fetched:
245 print(f"WARNING: post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
246 instances.update_last_error(domain, fetched)
248 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
249 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
250 instances.update_last_error(domain, fetched["json"]["error"]["message"])
253 rows = fetched["json"]
255 # DEBUG: print(f"DEBUG: rows({len(rows)})={rows} - blocked")
257 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
259 elif len(rows) != config.get("misskey_limit"):
260 # DEBUG: print(f"DEBUG: Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
261 offset = offset + (config.get("misskey_limit") - len(rows))
263 # DEBUG: print("DEBUG: Raising offset by step:", step)
264 offset = offset + step
267 for instance in rows:
269 # DEBUG: print(f"DEBUG: instance[{type(instance)}]='{instance}' - blocked")
270 if instance["isBlocked"] and not dicts.has_key(blocklist["blocked"], "domain", instance["host"]):
272 blocklist["blocked"].append({
273 "domain": tidyup.domain(instance["host"]),
277 # DEBUG: print(f"DEBUG: count={count}")
279 # DEBUG: print("DEBUG: API is no more returning new instances, aborting loop!")
282 except network.exceptions as exception:
283 print(f"WARNING: Caught error, exiting loop: domain='{domain}',exception[{type(exception)}]='{str(exception)}'")
284 instances.update_last_error(domain, exception)
288 # DEBUG: print(f"DEBUG: Updating last_instance_fetch for domain='{domain}' ...")
289 instances.update_last_instance_fetch(domain)
291 # DEBUG: print(f"DEBUG: Returning for domain='{domain}',blocked()={len(blocklist['blocked'])},suspended()={len(blocklist['suspended'])}")
293 "reject" : blocklist["blocked"],
294 "followers_only": blocklist["suspended"]