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/>.
20 from fba import network
22 from fba.helpers import blacklist
23 from fba.helpers import config
24 from fba.helpers import dicts
25 from fba.helpers import tidyup
27 from fba.models import instances
29 def fetch_peers(domain: str) -> list:
30 # DEBUG: print(f"DEBUG: domain({len(domain)})='{domain}' - CALLED!")
31 if not isinstance(domain, str):
32 raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
34 raise ValueError("Parameter 'domain' is empty")
36 # DEBUG: print(f"DEBUG: domain='{domain}' is misskey, sending API POST request ...")
39 step = config.get("misskey_limit")
41 # No CSRF by default, you don't have to add network.api_headers by yourself here
45 # DEBUG: print(f"DEBUG: Checking CSRF for domain='{domain}'")
46 headers = csrf.determine(domain, dict())
47 except network.exceptions as exception:
48 print(f"WARNING: Exception '{type(exception)}' during checking CSRF (fetch_peers,{__name__}) - EXIT!")
49 instances.set_last_error(domain, exception)
52 # iterating through all "suspended" (follow-only in its terminology)
53 # instances page-by-page, since that troonware doesn't support
54 # sending them all at once
56 # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
58 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
64 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
72 # DEBUG: print(f"DEBUG: fetched[]='{type(fetched)}'")
73 if "error_message" in fetched:
74 print(f"WARNING: post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
75 instances.set_last_error(domain, fetched)
77 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
78 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
79 instances.set_last_error(domain, fetched["json"]["error"]["message"])
82 rows = fetched["json"]
84 # DEBUG: print(f"DEBUG: rows()={len(rows)}")
86 # DEBUG: print(f"DEBUG: Returned zero bytes, exiting loop, domain='{domain}'")
88 elif len(rows) != config.get("misskey_limit"):
89 # DEBUG: print(f"DEBUG: Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
90 offset = offset + (config.get("misskey_limit") - len(rows))
92 # DEBUG: print(f"DEBUG: Raising offset by step={step}")
93 offset = offset + step
96 # DEBUG: print(f"DEBUG: rows({len(rows)})[]='{type(rows)}'")
98 # DEBUG: print(f"DEBUG: row()={len(row)}")
100 print(f"WARNING: row()={len(row)} does not contain key 'host': {row},domain='{domain}'")
102 elif not isinstance(row["host"], str):
103 print(f"WARNING: row[host][]='{type(row['host'])}' is not 'str'")
105 elif blacklist.is_blacklisted(row["host"]):
106 # DEBUG: print(f"DEBUG: row[host]='{row['host']}' is blacklisted. domain='{domain}'")
108 elif row["host"] in peers:
109 # DEBUG: print(f"DEBUG: Not adding row[host]='{row['host']}', already found.")
110 already = already + 1
113 # DEBUG: print(f"DEBUG: Adding peer: '{row['host']}'")
114 peers.append(row["host"])
116 if already == len(rows):
117 # DEBUG: print(f"DEBUG: Host returned same set of '{already}' instances, aborting loop!")
120 # DEBUG: print(f"DEBUG: Adding '{len(peers)}' for domain='{domain}'")
121 instances.set_total_peers(domain, peers)
123 # DEBUG: print(f"DEBUG: Returning peers[]='{type(peers)}'")
126 def fetch_blocks(domain: str) -> dict:
127 # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
128 if not isinstance(domain, str):
129 raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
131 raise ValueError("Parameter 'domain' is empty")
133 # DEBUG: print(f"DEBUG: Fetching misskey blocks from domain='{domain}'")
140 step = config.get("misskey_limit")
142 # No CSRF by default, you don't have to add network.api_headers by yourself here
146 # DEBUG: print(f"DEBUG: Checking CSRF for domain='{domain}'")
147 headers = csrf.determine(domain, dict())
148 except network.exceptions as exception:
149 print(f"WARNING: Exception '{type(exception)}' during checking CSRF (fetch_blocks,{__name__}) - EXIT!")
150 instances.set_last_error(domain, exception)
153 # iterating through all "suspended" (follow-only in its terminology)
154 # instances page-by-page since it doesn't support sending them all at once
157 # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
159 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
160 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
167 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
168 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
173 "offset" : offset - 1
176 # DEBUG: print(f"DEBUG: fetched[]='{type(fetched)}'")
177 if "error_message" in fetched:
178 print(f"WARNING: post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
179 instances.set_last_error(domain, fetched)
181 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
182 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
183 instances.set_last_error(domain, fetched["json"]["error"]["message"])
186 rows = fetched["json"]
188 # DEBUG: print(f"DEBUG: rows({len(rows)})={rows} - suspend")
190 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
192 elif len(rows) != config.get("misskey_limit"):
193 # DEBUG: print(f"DEBUG: Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
194 offset = offset + (config.get("misskey_limit") - len(rows))
196 # DEBUG: print("DEBUG: Raising offset by step:", step)
197 offset = offset + step
200 for instance in rows:
202 # DEBUG: print(f"DEBUG: instance[{type(instance)}]='{instance}' - suspend")
203 if "isSuspended" in instance and instance["isSuspended"] and not dicts.has_key(blocklist["suspended"], "domain", instance["host"]):
205 blocklist["suspended"].append({
206 "domain": tidyup.domain(instance["host"]),
207 # 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.set_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.set_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.set_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 "isBlocked" in instance and 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.set_last_error(domain, exception)
288 # DEBUG: print(f"DEBUG: Returning for domain='{domain}',blocked()={len(blocklist['blocked'])},suspended()={len(blocklist['suspended'])}")
290 "reject" : blocklist["blocked"],
291 "followers_only": blocklist["suspended"]