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/>.
26 from fba.helpers import config
27 from fba.helpers import cookies
28 from fba.helpers import domain as domain_helper
30 from fba.models import instances
32 logging.basicConfig(level=logging.INFO)
33 logger = logging.getLogger(__name__)
35 # HTTP headers for non-API requests
37 "User-Agent": config.get("useragent"),
40 # HTTP headers for API requests
42 "User-Agent" : config.get("useragent"),
43 "Content-Type": "application/json; charset=utf-8",
46 # Exceptions to always catch
48 requests.exceptions.ChunkedEncodingError,
49 requests.exceptions.ConnectionError,
50 requests.exceptions.InvalidSchema,
51 requests.exceptions.InvalidURL,
52 requests.exceptions.Timeout,
53 requests.exceptions.TooManyRedirects,
55 urllib3.exceptions.LocationParseError
58 def post_json_api(domain: str, path: str, data: str = "", headers: dict = dict()) -> dict:
59 logger.debug("domain='%s',path='%s',data='%s',headers()=%d - CALLED!", domain, path, data, len(headers))
60 domain_helper.raise_on(domain)
62 if not isinstance(path, str):
63 raise ValueError(f"path[]='{type(path)}' is not 'str'")
65 raise ValueError("Parameter 'path' cannot be empty")
66 elif not isinstance(data, str):
67 raise ValueError(f"data[]='{type(data)}' is not 'str'")
68 elif not isinstance(headers, dict):
69 raise ValueError(f"headers[]='{type(headers)}' is not 'list'")
76 logger.debug("Sending POST to domain='%s',path='%s',data='%s',headers(%d)='%s'", domain, path, data, len(headers), headers)
77 response = reqto.post(
78 f"https://{domain}{path}",
80 headers={**api_headers, **headers},
81 timeout=(config.get("connection_timeout"), config.get("read_timeout")),
82 cookies=cookies.get_all(domain) if cookies.has(domain) else dict()
85 logger.debug("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
86 json_reply["json"] = json_from_response(response)
88 logger.debug("response.ok='%s',response.status_code=%d,json_reply[]='%s'", response.ok, response.status_code, type(json_reply))
89 if not response.ok or response.status_code >= 400 or len(json_reply["json"]) == 0:
90 logger.warning("Cannot query JSON API: domain='%s',path='%s',data()=%d,response.status_code=%d,json_reply[]='%s'", domain, path, len(data), response.status_code, type(json_reply))
91 json_reply["status_code"] = response.status_code
92 json_reply["error_message"] = response.reason
93 instances.set_last_error(domain, response)
94 del json_reply["json"]
96 except exceptions as exception:
97 logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
98 json_reply["status_code"] = 999
99 json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
100 json_reply["exception"] = exception
101 instances.set_last_error(domain, exception)
104 logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
107 def fetch_api_url(url: str, timeout: tuple) -> dict:
108 logger.debug("url='%s',timeout()=%d - CALLED!", url, len(timeout))
109 if not isinstance(url, str):
110 raise ValueError(f"Parameter url[]='{type(url)}' is not 'str'")
112 raise ValueError("Parameter 'url' is empty")
113 elif not isinstance(timeout, tuple):
114 raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
121 logger.debug("Fetching url='%s' ...", url)
122 response = utils.fetch_url(url, api_headers, timeout)
124 logger.debug("Parsing JSON response from url='%s' ...", url)
125 json_reply["json"] = json_from_response(response)
127 logger.debug("response.ok='%s',response.status_code='%s',json_reply[]='%s'", response.ok, response.status_code, type(json_reply))
128 if not response.ok or response.status_code >= 400 or len(json_reply["json"]) == 0:
129 logger.warning("Cannot query JSON API: url='%s',response.status_code=%d,json_reply[]='%s'", url, response.status_code, type(json_reply))
130 json_reply["status_code"] = response.status_code
131 json_reply["error_message"] = response.reason
132 del json_reply["json"]
134 except exceptions as exception:
135 logger.debug("Fetching url='%s' failed. exception[%s]='%s'", url, type(exception), str(exception))
136 json_reply["status_code"] = 999
137 json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
138 json_reply["exception"] = exception
141 logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
144 def get_json_api(domain: str, path: str, headers: dict, timeout: tuple) -> dict:
145 logger.debug("domain='%s',path='%s',timeout()=%d - CALLED!", domain, path, len(timeout))
146 domain_helper.raise_on(domain)
148 if not isinstance(path, str):
149 raise ValueError(f"path[]='{type(path)}' is not 'str'")
151 raise ValueError("Parameter 'path' cannot be empty")
152 elif not isinstance(headers, dict):
153 raise ValueError(f"headers[]='{type(headers)}' is not 'list'")
154 elif not isinstance(timeout, tuple):
155 raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
162 logger.debug("Sending GET to domain='%s',path='%s',timeout(%d)='%s'", domain, path, len(timeout), timeout)
163 response = reqto.get(
164 f"https://{domain}{path}",
165 headers={**api_headers, **headers},
167 cookies=cookies.get_all(domain) if cookies.has(domain) else {}
170 except exceptions as exception:
171 logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
172 json_reply["status_code"] = 999
173 json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
174 json_reply["exception"] = exception
175 instances.set_last_error(domain, exception)
178 logger.debug("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
179 json_reply["json"] = json_from_response(response)
181 logger.debug("response.ok='%s',response.status_code=%d,json_reply[]='%s'", response.ok, response.status_code, type(json_reply))
182 if not response.ok or response.status_code >= 400 or len(json_reply["json"]) == 0:
183 logger.warning("Cannot query JSON API: domain='%s',path='%s',response.status_code=%d,json_reply[]='%s'", domain, path, response.status_code, type(json_reply))
184 json_reply["status_code"] = response.status_code
185 json_reply["error_message"] = response.reason
186 del json_reply["json"]
187 instances.set_last_error(domain, response)
189 logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
192 def send_bot_post(domain: str, blocklist: dict):
193 logger.debug("domain='%s',blocklist()=%d - CALLED!", domain, len(blocklist))
194 domain_helper.raise_on(domain)
196 if not isinstance(blocklist, dict):
197 raise ValueError(f"Parameter blocklist[]='{type(blocklist)}' is not 'dict'")
199 message = f"{domain} has blocked the following instances:\n\n"
202 if len(blocklist) > 20:
204 blocklist = blocklist[0 : 19]
206 logger.debug("blocklist()=%d", len(blocklist))
207 for block in blocklist:
208 logger.debug("block[%s]='%s'", type(block), block)
209 if block["reason"] is None or block["reason"] == '':
210 message = message + block["blocked"] + " with unspecified reason\n"
212 if len(block["reason"]) > 420:
213 block["reason"] = block["reason"][0:419] + "[…]"
215 message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
218 message = message + "(the list has been truncated to the first 20 entries)"
220 botheaders = {**api_headers, **{"Authorization": "Bearer " + config.get("bot_token")}}
223 f"{config.get('bot_instance')}/api/v1/statuses",
226 "visibility" : config.get('bot_visibility'),
227 "content_type": "text/plain"
235 def fetch_response(domain: str, path: str, headers: dict, timeout: tuple) -> requests.models.Response:
236 logger.debug("domain='%s',path='%s',headers()=%d,timeout='%s' - CALLED!", domain, path, len(headers), timeout)
237 domain_helper.raise_on(domain)
239 if not isinstance(path, str):
240 raise ValueError(f"Parameter path[]='{type(path)}' is not 'str'")
242 raise ValueError("Parameter 'path' is empty")
243 elif not isinstance(headers, dict):
244 raise ValueError(f"headers[]='{type(headers)}' is not 'dict'")
245 elif not isinstance(timeout, tuple):
246 raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
249 logger.debug("Sending GET request to '%s%s' ...", domain, path)
250 response = reqto.get(
251 f"https://{domain}{path}",
254 cookies=cookies.get_all(domain) if cookies.has(domain) else {}
257 except exceptions as exception:
258 logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
259 instances.set_last_error(domain, exception)
262 logger.debug("response[]='%s' - EXIT!", type(response))
265 def json_from_response(response: requests.models.Response) -> list:
266 logger.debug("response[]='%s' - CALLED!", type(response))
267 if not isinstance(response, requests.models.Response):
268 raise ValueError(f"Parameter response[]='{type(response)}' is not type of 'Response'")
269 elif response.headers.get("content-type") is None or response.headers.get("content-type").split(";")[0] != "application/json":
270 logger.warning("response.headers[content-type]='%s' is not a JSON type, below json() invocation may raise an exception", response.headers.get("content-type"))
273 if response.text.strip() != "":
274 logger.debug("response.text()=%d is not empty, invoking response.json() ...", len(response.text))
276 data = response.json()
277 except json.decoder.JSONDecodeError as exception:
278 logger.warning("Exception '%s' during decoding JSON from response.url='%s'", type(exception), response.url)
280 logger.debug("data[]='%s' - EXIT!", type(data))