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")
41 headers = csrf.determine(domain, {"Origin": domain})
43 # iterating through all "suspended" (follow-only in its terminology)
44 # instances page-by-page, since that troonware doesn't support
45 # sending them all at once
47 # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
49 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
55 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
63 # DEBUG: print(f"DEBUG: fetched[]={type(fetched)}")
64 if "error_message" in fetched:
65 print(f"WARNING: post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
66 instances.update_last_error(domain, fetched)
68 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
69 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
70 instances.update_last_error(domain, fetched["json"]["error"]["message"])
73 rows = fetched["json"]
75 # DEBUG: print(f"DEBUG: rows()={len(rows)}")
77 # DEBUG: print(f"DEBUG: Returned zero bytes, exiting loop, domain='{domain}'")
79 elif len(rows) != config.get("misskey_limit"):
80 # DEBUG: print(f"DEBUG: Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
81 offset = offset + (config.get("misskey_limit") - len(rows))
83 # DEBUG: print(f"DEBUG: Raising offset by step={step}")
84 offset = offset + step
87 # DEBUG: print(f"DEBUG: rows({len(rows)})[]={type(rows)}")
89 # DEBUG: print(f"DEBUG: row()={len(row)}")
91 print(f"WARNING: row()={len(row)} does not contain key 'host': {row},domain='{domain}'")
93 elif not isinstance(row["host"], str):
94 print(f"WARNING: row[host][]={type(row['host'])} is not 'str'")
96 elif blacklist.is_blacklisted(row["host"]):
97 # DEBUG: print(f"DEBUG: row[host]='{row['host']}' is blacklisted. domain='{domain}'")
99 elif row["host"] in peers:
100 # DEBUG: print(f"DEBUG: Not adding row[host]='{row['host']}', already found.")
101 already = already + 1
104 # DEBUG: print(f"DEBUG: Adding peer: '{row['host']}'")
105 peers.append(row["host"])
107 if already == len(rows):
108 # DEBUG: print(f"DEBUG: Host returned same set of '{already}' instances, aborting loop!")
111 # DEBUG: print(f"DEBUG: Adding '{len(peers)}' for domain='{domain}'")
112 instances.set_data("total_peers", domain, len(peers))
114 # DEBUG: print(f"DEBUG: Updating last_instance_fetch for domain='{domain}' ...")
115 instances.update_last_instance_fetch(domain)
117 # DEBUG: print(f"DEBUG: Returning peers[]='{type(peers)}'")
120 def fetch_blocks(domain: str) -> dict:
121 # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
122 if not isinstance(domain, str):
123 raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
125 raise ValueError("Parameter 'domain' is empty")
127 # DEBUG: print(f"DEBUG: Fetching misskey blocks from domain={domain}")
134 step = config.get("misskey_limit")
135 headers = csrf.determine(domain, {"Origin": domain})
137 # iterating through all "suspended" (follow-only in its terminology)
138 # instances page-by-page since it doesn't support sending them all at once
141 # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
143 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
144 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
151 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
152 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
157 "offset" : offset - 1
160 # DEBUG: print(f"DEBUG: fetched[]={type(fetched)}")
161 if "error_message" in fetched:
162 print(f"WARNING: post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
163 instances.update_last_error(domain, fetched)
165 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
166 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
167 instances.update_last_error(domain, fetched["json"]["error"]["message"])
170 rows = fetched["json"]
172 # DEBUG: print(f"DEBUG: rows({len(rows)})={rows} - suspend")
174 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
176 elif len(rows) != config.get("misskey_limit"):
177 # DEBUG: print(f"DEBUG: Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
178 offset = offset + (config.get("misskey_limit") - len(rows))
180 # DEBUG: print("DEBUG: Raising offset by step:", step)
181 offset = offset + step
184 for instance in rows:
186 # DEBUG: print(f"DEBUG: instance[{type(instance)}]='{instance}' - suspend")
187 if instance["isSuspended"] and not dicts.has_key(blocklist["suspended"], "domain", instance["host"]):
189 blocklist["suspended"].append(
191 "domain": tidyup.domain(instance["host"]),
192 # no reason field, nothing
197 # DEBUG: print(f"DEBUG: count={count}")
199 # DEBUG: print("DEBUG: API is no more returning new instances, aborting loop!")
202 except network.exceptions as exception:
203 print(f"WARNING: Caught error, exiting loop: domain='{domain}',exception[{type(exception)}]='{str(exception)}'")
204 instances.update_last_error(domain, exception)
209 # Fetch blocked (full suspended) instances
212 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
213 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
220 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
221 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
226 "offset" : offset - 1
229 # DEBUG: print(f"DEBUG: fetched[]={type(fetched)}")
230 if "error_message" in fetched:
231 print(f"WARNING: post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
232 instances.update_last_error(domain, fetched)
234 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
235 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
236 instances.update_last_error(domain, fetched["json"]["error"]["message"])
239 rows = fetched["json"]
241 # DEBUG: print(f"DEBUG: rows({len(rows)})={rows} - blocked")
243 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
245 elif len(rows) != config.get("misskey_limit"):
246 # DEBUG: print(f"DEBUG: Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
247 offset = offset + (config.get("misskey_limit") - len(rows))
249 # DEBUG: print("DEBUG: Raising offset by step:", step)
250 offset = offset + step
253 for instance in rows:
255 # DEBUG: print(f"DEBUG: instance[{type(instance)}]='{instance}' - blocked")
256 if instance["isBlocked"] and not dicts.has_key(blocklist["blocked"], "domain", instance["host"]):
258 blocklist["blocked"].append({
259 "domain": tidyup.domain(instance["host"]),
263 # DEBUG: print(f"DEBUG: count={count}")
265 # DEBUG: print("DEBUG: API is no more returning new instances, aborting loop!")
268 except network.exceptions as exception:
269 print(f"WARNING: Caught error, exiting loop: domain='{domain}',exception[{type(exception)}]='{str(exception)}'")
270 instances.update_last_error(domain, exception)
274 # DEBUG: print(f"DEBUG: Updating last_instance_fetch for domain='{domain}' ...")
275 instances.update_last_instance_fetch(domain)
277 # DEBUG: print(f"DEBUG: Returning for domain='{domain}',blocked()={len(blocklist['blocked'])},suspended()={len(blocklist['suspended'])}")
279 "reject" : blocklist["blocked"],
280 "followers_only": blocklist["suspended"]