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/>.
23 from fba.helpers import config
24 from fba.helpers import dicts as dict_helper
25 from fba.helpers import domain as domain_helper
26 from fba.helpers import tidyup
28 from fba.http import network
30 from fba.models import instances
32 logging.basicConfig(level=logging.INFO)
33 logger = logging.getLogger(__name__)
35 def fetch_peers(domain: str) -> list:
36 logger.debug("domain='%s' - CALLED!", domain)
37 domain_helper.raise_on(domain)
39 logger.debug("domain='%s' is misskey, sending API POST request ...", domain)
42 step = config.get("misskey_limit")
44 # No CSRF by default, you don't have to add network.api_headers by yourself here
48 logger.debug("Checking CSRF for domain='%s'", domain)
49 headers = csrf.determine(domain, dict())
50 except network.exceptions as exception:
51 logger.warning("Exception '%s' during checking CSRF (fetch_peers,%s)", type(exception), __name__)
52 instances.set_last_error(domain, exception)
54 logger.debug("Returning empty list ... - EXIT!")
57 # iterating through all "suspended" (follow-only in its terminology)
58 # instances page-by-page, since that troonware doesn't support
59 # sending them all at once
61 logger.debug("Fetching offset=%d from domain='%s' ...", offset, domain)
63 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
69 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
77 logger.debug("fetched[]='%s'", type(fetched))
78 if "error_message" in fetched:
79 logger.warning("post_json_api() for domain='%s' returned error message: '%s'", domain, fetched['error_message'])
80 instances.set_last_error(domain, fetched)
82 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
83 logger.warning("post_json_api() returned error: '%s'", fetched['error']['message'])
84 instances.set_last_error(domain, fetched["json"]["error"]["message"])
87 rows = fetched["json"]
89 logger.debug("rows(%d)[]='%s',step=%d", len(rows), type(rows), step)
91 logger.debug("Returned zero bytes, domain='%s' - BREAK!", domain)
93 elif len(rows) != config.get("misskey_limit"):
94 logger.debug("Fetched %d row(s) but expected: %d", len(rows), config.get('misskey_limit'))
95 offset = offset + (config.get("misskey_limit") - len(rows))
97 logger.debug("Raising offset by step=%d", step)
98 offset = offset + step
101 logger.debug("rows(%d))[]='%s'", len(rows), type(rows))
103 logger.debug("row()=%d", len(row))
104 if "host" not in row:
105 logger.warning("row()=%d does not contain key 'host': row='%s',domain='%s' - SKIPPED!", len(row), row, domain)
107 elif not isinstance(row["host"], str):
108 logger.warning("row[host][]='%s' is not of type 'str' - SKIPPED!", type(row['host']))
110 elif not utils.is_domain_wanted(row["host"]):
111 logger.debug("row[host]='%s' is not wanted, domain='%s' - SKIPPED!", row['host'], domain)
113 elif row["host"] in peers:
114 logger.debug("Not adding row[host]='%s', already found - SKIPPED!", row['host'])
117 logger.debug("Adding peer: row[host]='%s'", row['host'])
119 peers.append(row["host"])
121 logger.debug("added=%d,rows()=%d", added, len(rows))
123 logger.debug("Host returned already added (%d) peers - BREAK!", len(rows))
126 logger.debug("peers()=%d - EXIT!", len(peers))
129 def fetch_blocks(domain: str) -> list:
130 logger.debug("domain='%s' - CALLED!", domain)
131 domain_helper.raise_on(domain)
133 # No CSRF by default, you don't have to add network.api_headers by yourself here
137 logger.debug("Checking CSRF for domain='%s'", domain)
138 headers = csrf.determine(domain, dict())
139 except network.exceptions as exception:
140 logger.warning("Exception '%s' during checking CSRF (fetch_blocks,%s)", type(exception), __name__)
141 instances.set_last_error(domain, exception)
143 logger.debug("Returning empty list ... - EXIT!")
148 step = config.get("misskey_limit")
150 # iterating through all "suspended" (follow-only in its terminology)
151 # instances page-by-page since it doesn't support sending them all at once
152 logger.debug("Fetching misskey blocks from domain='%s'", domain)
155 logger.debug("Fetching offset=%d from domain='%s' ...", offset, domain)
157 logger.debug("Sending JSON API request to domain='%s',step=%d,offset=%d", domain, step, offset)
158 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
165 logger.debug("Sending JSON API request to domain='%s',step=%d,offset=%d", domain, step, offset)
166 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
171 "offset" : offset - 1
174 logger.debug("fetched[]='%s'", type(fetched))
175 if "error_message" in fetched:
176 logger.warning("post_json_api() for domain='%s' returned error message: '%s'", domain, fetched['error_message'])
177 instances.set_last_error(domain, fetched)
179 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
180 logger.warning("post_json_api() returned error: '%s'", fetched['error']['message'])
181 instances.set_last_error(domain, fetched["json"]["error"]["message"])
184 rows = fetched["json"]
186 logger.debug("rows(%d)[]='%s'", len(rows), type(rows))
188 logger.debug("Returned zero bytes, domain='%s' - BREAK!", domain)
190 elif len(rows) != config.get("misskey_limit"):
191 logger.debug("Fetched %d row(s) but expected: %d", len(rows), config.get('misskey_limit'))
192 offset = offset + (config.get("misskey_limit") - len(rows))
194 logger.debug("Raising offset by step=%d", step)
195 offset = offset + step
198 for instance in rows:
200 logger.debug("instance[]='%s'", type(instance))
201 blocked = tidyup.domain(instance["host"])
202 if "isSuspended" in instance and instance["isSuspended"] and not dict_helper.has_key(blocklist, "blocked", blocked):
204 logger.debug("Appending blocker='%s',blocked='%s',block_level='suspended'", domain, blocked)
209 "block_level": "suspended",
212 logger.debug("count=%d", count)
214 logger.debug("API is no more returning new instances, aborting loop! domain='%s'", domain)
217 except network.exceptions as exception:
218 logger.warning("Caught error, exiting loop: domain='%s',exception[%s]='%s'", domain, type(exception), str(exception))
219 instances.set_last_error(domain, exception)
224 # Fetch blocked (full suspended) instances
227 logger.debug("Sending JSON API request to domain='%s',step=%d,offset=%d", domain, step, offset)
228 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
235 logger.debug("Sending JSON API request to domain='%s',step=%d,offset=%d", domain, step, offset)
236 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
241 "offset" : offset - 1
244 logger.debug("fetched[]='%s'", type(fetched))
245 if "error_message" in fetched:
246 logger.warning("post_json_api() for domain='%s' returned error message: '%s'", domain, fetched['error_message'])
247 instances.set_last_error(domain, fetched)
249 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
250 logger.warning("post_json_api() returned error: '%s'", fetched['error']['message'])
251 instances.set_last_error(domain, fetched["json"]["error"]["message"])
254 rows = fetched["json"]
256 logger.debug("rows(%d)[]='%s'", len(rows), type(rows))
258 logger.debug("Returned zero bytes, domain='%s' - BREAK!", domain)
260 elif len(rows) != config.get("misskey_limit"):
261 logger.debug("Fetched %d row(s) but expected: %d'", len(rows), config.get('misskey_limit'))
262 offset = offset + (config.get("misskey_limit") - len(rows))
264 logger.debug("Raising offset by step=%d", step)
265 offset = offset + step
268 for instance in rows:
270 logger.debug("instance[]='%s'", type(instance))
271 blocked = tidyup.domain(instance["host"])
272 if "isBlocked" in instance and instance["isBlocked"] and not dict_helper.has_key(blocklist, "blocked", blocked):
274 logger.debug("Appending blocker='%s',blocked='%s',block_level='reject'", domain, blocked)
279 "block_level": "reject",
282 logger.debug("count=%d", count)
284 logger.debug("API is no more returning new instances, aborting loop!")
287 except network.exceptions as exception:
288 logger.warning("Caught error, exiting loop: domain='%s',exception[%s]='%s'", domain, type(exception), str(exception))
289 instances.set_last_error(domain, exception)
293 logger.debug("blocklist()=%d - EXIT!", len(blocklist))