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.helpers import blacklist
21 from fba.helpers import config
22 from fba.helpers import dicts as dict_helper
23 from fba.helpers import domain as domain_helper
24 from fba.helpers import tidyup
26 from fba.http import csrf
27 from fba.http import network
29 from fba.models import instances
31 logging.basicConfig(level=logging.INFO)
32 logger = logging.getLogger(__name__)
33 #logger.setLevel(logging.DEBUG)
35 def fetch_peers(domain: str) -> list:
36 logger.debug("domain='%s' - CALLED!", domain)
37 domain_helper.raise_on(domain)
39 if blacklist.is_blacklisted(domain):
40 raise RuntimeError(f"domain='{domain}' is blacklisted but function was invoked")
41 elif not instances.is_registered(domain):
42 raise RuntimeError(f"domain='{domain}' is not registered but function was invoked")
44 logger.debug("domain='%s' is misskey, sending API POST request ...", domain)
47 step = config.get("misskey_limit")
49 # No CSRF by default, you don't have to add network.api_headers by yourself here
53 logger.debug("Checking CSRF for domain='%s'", domain)
54 headers = csrf.determine(domain, {})
55 except network.exceptions as exception:
56 logger.warning("Exception '%s' during checking CSRF (fetch_peers,%s)", type(exception), __name__)
57 instances.set_last_error(domain, exception)
59 logger.debug("Returning empty list ... - EXIT!")
62 # iterating through all "suspended" (follow-only in its terminology)
63 # instances page-by-page, since that troonware doesn't support
64 # sending them all at once
66 logger.debug("Fetching offset=%d from domain='%s' ...", offset, domain)
68 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
74 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
82 logger.debug("fetched[]='%s'", type(fetched))
83 if "error_message" in fetched:
84 logger.warning("post_json_api() for domain='%s' returned error message: '%s'", domain, fetched['error_message'])
85 instances.set_last_error(domain, fetched)
87 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
88 logger.warning("post_json_api() returned error: '%s'", fetched["json"]["error"]["message"])
89 instances.set_last_error(domain, fetched["json"]["error"]["message"])
92 rows = fetched["json"]
94 logger.debug("rows(%d)[]='%s',step=%d", len(rows), type(rows), step)
96 logger.debug("Returned zero bytes, domain='%s' - BREAK!", domain)
98 elif len(rows) != step:
99 logger.debug("Fetched %d row(s) but expected: %d", len(rows), step)
100 offset = offset + (step - len(rows))
102 logger.debug("Raising offset by step=%d", step)
103 offset = offset + step
106 logger.debug("rows(%d))[]='%s'", len(rows), type(rows))
108 logger.debug("row()=%d", len(row))
109 if "host" not in row:
110 logger.warning("row()=%d does not contain key 'host': row='%s',domain='%s' - SKIPPED!", len(row), row, domain)
112 elif not isinstance(row["host"], str):
113 logger.warning("row[host][]='%s' has not expected type 'str' - SKIPPED!", type(row["host"]))
115 elif row["host"] == "":
116 logger.warning("row[host] is an empty string,domain='%s' - SKIPPED!", domain)
118 elif row["host"] in peers:
119 logger.debug("Not adding row[host]='%s', already found - SKIPPED!", row["host"])
121 elif not domain_helper.is_wanted(row["host"]):
122 logger.debug("row[host]='%s' is not wanted - SKIPPED!", row["host"])
125 logger.debug("Adding peer: row[host]='%s'", row["host"])
127 peers.append(row["host"])
129 logger.debug("added=%d,rows()=%d", added, len(rows))
131 logger.debug("Host returned already added (%d) peers - BREAK!", len(rows))
134 logger.debug("peers()=%d - EXIT!", len(peers))
137 def fetch_blocks(domain: str) -> list:
138 logger.debug("domain='%s' - CALLED!", domain)
139 domain_helper.raise_on(domain)
141 if blacklist.is_blacklisted(domain):
142 raise RuntimeError(f"domain='{domain}' is blacklisted but function was invoked")
143 elif not instances.is_registered(domain):
144 raise RuntimeError(f"domain='{domain}' is not registered but function was invoked")
146 # No CSRF by default, you don't have to add network.api_headers by yourself here
150 logger.debug("Checking CSRF for domain='%s' ...", domain)
151 headers = csrf.determine(domain, {})
152 except network.exceptions as exception:
153 logger.warning("Exception '%s' during checking CSRF (fetch_blocks,%s)", type(exception), __name__)
154 instances.set_last_error(domain, exception)
156 logger.debug("Returning empty list ... - EXIT!")
161 step = config.get("misskey_limit")
163 # iterating through all "suspended" (follow-only in its terminology)
164 # instances page-by-page since it doesn't support sending them all at once
165 logger.debug("Fetching misskey blocks from domain='%s' ...", domain)
167 logger.debug("offset=%d", offset)
169 logger.debug("Fetching offset=%d from domain='%s' ...", offset, domain)
171 logger.debug("Sending JSON API request to domain='%s',step=%d ...", domain, step)
172 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
178 logger.debug("Sending JSON API request to domain='%s',step=%d,offset=%d ...", domain, step, offset)
179 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
183 "offset" : offset - 1
186 logger.debug("fetched[]='%s'", type(fetched))
187 if "error_message" in fetched:
188 logger.warning("post_json_api() for domain='%s' returned error message: '%s'", domain, fetched['error_message'])
189 instances.set_last_error(domain, fetched)
191 elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
192 logger.warning("post_json_api() returned error: '%s'", fetched["json"]["error"]["message"])
193 instances.set_last_error(domain, fetched["json"]["error"]["message"])
196 rows = fetched["json"]
198 logger.debug("rows(%d)[]='%s'", len(rows), type(rows))
200 logger.debug("Returned zero bytes, domain='%s' - BREAK!", domain)
202 elif len(rows) != step:
203 logger.debug("Fetched %d row(s) but expected: %d", len(rows), step)
204 offset = offset + (step - len(rows))
206 logger.debug("Raising offset by step=%d", step)
207 offset = offset + step
210 logger.debug("Checking %d row(s) of instances ...", len(rows))
211 for instance in rows:
213 logger.debug("instance[]='%s'", type(instance))
214 if "host" not in instance:
215 logger.warning("instance(%d)='%s' has no key 'host' - SKIPPED!", len(instance), instance)
217 elif not isinstance(instance["host"], str):
218 logger.warning("instance[host][]='%s' has not expected type 'str' - SKIPPED!", type(instance["host"]))
220 elif instance["host"] == "":
221 logger.warning("instance[host] is an empty string,domain='%s' - SKIPPED!", domain)
224 logger.debug("instance[host]='%s' - BEFORE!", instance["host"])
225 blocked = tidyup.domain(instance["host"])
226 logger.debug("blocked[%s]='%s' - AFTER!", type(blocked), blocked)
228 if blocked in [None, ""]:
229 logger.warning("instance[host]='%s' is None or empty after tidyup.domain() - SKIPPED!", instance["host"])
231 elif not domain_helper.is_wanted(blocked):
232 logger.debug("blocked='%s' is not wanted - SKIPPED!", blocked)
234 elif "isSuspended" in instance and instance["isSuspended"] and not dict_helper.has_key(blocklist, "blocked", blocked):
236 logger.debug("Appending blocker='%s',blocked='%s',block_level='suspended' ... #1", domain, blocked)
241 "block_level": "suspended",
243 elif "isBlocked" in instance and instance["isBlocked"] and not dict_helper.has_key(blocklist, "blocked", blocked):
245 logger.debug("Appending blocker='%s',blocked='%s',block_level='suspended' ... #2", domain, blocked)
250 "block_level": "suspended",
252 elif "isSilenced" in instance and instance["isSilenced"] and not dict_helper.has_key(blocklist, "blocked", blocked):
254 logger.debug("Appending blocker='%s',blocked='%s',block_level='silenced' ...", domain, blocked)
259 "block_level": "silenced",
262 logger.debug("domain='%s',blocked='%s' is not marked suspended - SKIPPED!", domain, blocked)
265 logger.debug("count=%d", count)
267 logger.debug("API is no more returning new instances, aborting loop! domain='%s'", domain)
270 except network.exceptions as exception:
271 logger.warning("Caught error, exiting loop: domain='%s',exception[%s]='%s'", domain, type(exception), str(exception))
272 instances.set_last_error(domain, exception)
276 logger.debug("blocklist()=%d - EXIT!", len(blocklist))