]> git.mxchange.org Git - fba.git/blob - fba/network.py
Continued:
[fba.git] / fba / network.py
1 # Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
2 # Copyright (C) 2023 Free Software Foundation
3 #
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.
8 #
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.
13 #
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/>.
16
17 import json
18 import reqto
19 import requests
20
21 from fba import config
22 from fba import fba
23 from fba import instances
24
25 # HTTP headers for non-API requests
26 web_headers = {
27     "User-Agent": config.get("useragent"),
28 }
29
30 # HTTP headers for API requests
31 api_headers = {
32     "User-Agent"  : config.get("useragent"),
33     "Content-Type": "application/json",
34 }
35
36 # Exceptions to always catch
37 exceptions = (
38     requests.exceptions.ChunkedEncodingError,
39     requests.exceptions.ConnectionError,
40     requests.exceptions.Timeout,
41     requests.exceptions.TooManyRedirects,
42     UnicodeEncodeError
43 )
44
45 def post_json_api(domain: str, path: str, data: str, headers: dict = {}) -> dict:
46     # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',data='{data}',headers()={len(headers)} - CALLED!")
47     if not isinstance(domain, str):
48         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
49     elif domain == "":
50         raise ValueError("Parameter 'domain' is empty")
51     elif not isinstance(path, str):
52         raise ValueError(f"path[]='{type(path)}' is not 'str'")
53     elif path == "":
54         raise ValueError("Parameter 'path' cannot be empty")
55     elif not isinstance(data, str):
56         raise ValueError(f"data[]='{type(data)}' is not 'str'")
57     elif not isinstance(headers, dict):
58         raise ValueError(f"headers[]='{type(headers)}' is not 'list'")
59
60     json_reply = {
61         "status_code": 200,
62     }
63
64     try:
65         # DEBUG: print(f"DEBUG: Sending POST to domain='{domain}',path='{path}',data='{data}',headers({len(headers)})={headers}")
66         response = reqto.post(
67             f"https://{domain}{path}",
68             data=data,
69             headers={**api_headers, **headers},
70             timeout=(config.get("connection_timeout"), config.get("read_timeout"))
71         )
72
73         json_reply["json"] = json_from_response(response)
74
75         # DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},json_reply[]='{type(json_reply)}'")
76         if not response.ok or response.status_code >= 400:
77             print(f"WARNING: Cannot query JSON API: domain='{domain}',path='{path}',data()={len(data)},response.status_code='{response.status_code}',json_reply[]='{type(json_reply)}'")
78             json_reply["status_code"]   = response.status_code
79             json_reply["error_message"] = response.reason
80             del json_reply["json"]
81             instances.update_last_error(domain, response)
82
83     except exceptions as exception:
84         # DEBUG: print(f"DEBUG: Fetching '{path}' from '{domain}' failed. exception[{type(exception)}]='{str(exception)}'")
85         json_reply["status_code"]   = 999
86         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
87         json_reply["exception"]     = exception
88         instances.update_last_error(domain, exception)
89         raise exception
90
91     # DEBUG: print(f"DEBUG: Returning json_reply({len(json_reply)})=[]:{type(json_reply)}")
92     return json_reply
93
94 def fetch_api_url(url: str, timeout: tuple) -> dict:
95     # DEBUG: print(f"DEBUG: url='{url}',timeout()={len(timeout)} - CALLED!")
96     if not isinstance(url, str):
97         raise ValueError(f"Parameter url[]='{type(url)}' is not 'str'")
98     elif not isinstance(timeout, tuple):
99         raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
100
101     json_reply = {
102        "status_code": 200,
103     }
104
105     try:
106         # DEBUG: print(f"DEBUG: Fetching url='{url}' ...")
107         response = fba.fetch_url(url, api_headers, timeout)
108
109         json_reply["json"] = json_from_response(response)
110
111         # DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},json_reply[]='{type(json_reply)}'")
112         if not response.ok or response.status_code >= 400:
113             print(f"WARNING: Cannot query JSON API: url='{url}',response.status_code='{response.status_code}',json_reply[]='{type(json_reply)}'")
114             json_reply["status_code"]   = response.status_code
115             json_reply["error_message"] = response.reason
116             del json_reply["json"]
117
118     except exceptions as exception:
119         # DEBUG: print(f"DEBUG: Fetching '{url}' failed. exception[{type(exception)}]='{str(exception)}'")
120         json_reply["status_code"]   = 999
121         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
122         json_reply["exception"]     = exception
123         raise exception
124
125     # DEBUG: print(f"DEBUG: Returning json_reply({len(json_reply)})=[]:{type(json_reply)}")
126     return json_reply
127
128 def get_json_api(domain: str, path: str, headers: dict, timeout: tuple) -> dict:
129     # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',timeout()={len(timeout)} - CALLED!")
130     if not isinstance(domain, str):
131         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
132     elif domain == "":
133         raise ValueError("Parameter 'domain' is empty")
134     elif not isinstance(path, str):
135         raise ValueError(f"path[]='{type(path)}' is not 'str'")
136     elif path == "":
137         raise ValueError("Parameter 'path' cannot be empty")
138     elif not isinstance(headers, dict):
139         raise ValueError(f"headers[]='{type(headers)}' is not 'list'")
140     elif not isinstance(timeout, tuple):
141         raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
142
143     json_reply = {
144         "status_code": 200,
145     }
146
147     try:
148         # DEBUG: print(f"DEBUG: Sending GET to domain='{domain}',path='{path}',timeout({len(timeout)})={timeout}")
149         response = reqto.get(
150             f"https://{domain}{path}",
151             headers={**api_headers, **headers},
152             timeout=timeout
153         )
154
155     except exceptions as exception:
156         # DEBUG: print(f"DEBUG: Fetching '{path}' from '{domain}' failed. exception[{type(exception)}]='{str(exception)}'")
157         json_reply["status_code"]   = 999
158         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
159         json_reply["exception"]     = exception
160         instances.update_last_error(domain, exception)
161         raise exception
162
163     json_reply["json"] = json_from_response(response)
164
165     # DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},json_reply[]='{type(json_reply)}'")
166     if not response.ok or response.status_code >= 400:
167         print(f"WARNING: Cannot query JSON API: domain='{domain}',path='{path}',response.status_code='{response.status_code}',json_reply[]='{type(json_reply)}'")
168         json_reply["status_code"]   = response.status_code
169         json_reply["error_message"] = response.reason
170         del json_reply["json"]
171         instances.update_last_error(domain, response)
172
173     # DEBUG: print(f"DEBUG: Returning json_reply({len(json_reply)})=[]:{type(json_reply)}")
174     return json_reply
175
176 def send_bot_post(domain: str, blocklist: dict):
177     # DEBUG: print(f"DEBUG: domain={domain},blocklist()={len(blocklist)} - CALLED!")
178     if not isinstance(domain, str):
179         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
180     elif domain == "":
181         raise ValueError("Parameter 'domain' is empty")
182     elif not isinstance(blocklist, dict):
183         raise ValueError(f"Parameter blocklist[]='{type(blocklist)}' is not 'dict'")
184
185     message = f"{domain} has blocked the following instances:\n\n"
186     truncated = False
187
188     if len(blocklist) > 20:
189         truncated = True
190         blocklist = blocklist[0 : 19]
191
192     # DEBUG: print(f"DEBUG: blocklist()={len(blocklist)}")
193     for block in blocklist:
194         # DEBUG: print(f"DEBUG: block['{type(block)}']={block}")
195         if block["reason"] is None or block["reason"] == '':
196             message = message + block["blocked"] + " with unspecified reason\n"
197         else:
198             if len(block["reason"]) > 420:
199                 block["reason"] = block["reason"][0:419] + "[…]"
200
201             message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
202
203     if truncated:
204         message = message + "(the list has been truncated to the first 20 entries)"
205
206     botheaders = {**api_headers, **{"Authorization": "Bearer " + config.get("bot_token")}}
207
208     req = reqto.post(
209         f"{config.get('bot_instance')}/api/v1/statuses",
210         data={
211             "status"      : message,
212             "visibility"  : config.get('bot_visibility'),
213             "content_type": "text/plain"
214         },
215         headers=botheaders,
216         timeout=10
217     ).json()
218
219     return True
220
221 def fetch_response(domain: str, path: str, headers: dict, timeout: tuple) -> requests.models.Response:
222     # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',headers()={len(headers)},timeout={timeout} - CALLED!")
223     if not isinstance(domain, str):
224         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
225     elif domain == "":
226         raise ValueError("Parameter 'domain' is empty")
227     elif not isinstance(path, str):
228         raise ValueError(f"Parameter path[]='{type(path)}' is not 'str'")
229     elif path == "":
230         raise ValueError("Parameter 'path' is empty")
231     elif not isinstance(headers, dict):
232         raise ValueError(f"headers[]='{type(headers)}' is not 'dict'")
233     elif not isinstance(timeout, tuple):
234         raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
235
236     try:
237         # DEBUG: print(f"DEBUG: Sending GET request to '{domain}{path}' ...")
238         response = reqto.get(
239             f"https://{domain}{path}",
240             headers=headers,
241             timeout=timeout
242         )
243
244     except exceptions as exception:
245         # DEBUG: print(f"DEBUG: Fetching '{path}' from '{domain}' failed. exception[{type(exception)}]='{str(exception)}'")
246         instances.update_last_error(domain, exception)
247         raise exception
248
249     # DEBUG: print(f"DEBUG: response[]='{type(response)}' - EXXIT!")
250     return response
251
252 def json_from_response(response: requests.models.Response) -> list:
253     # DEBUG: print(f"DEBUG: response[]='{type(response)}' - CALLED!")
254     if not isinstance(response, requests.models.Response):
255         raise ValueError(f"Parameter response[]='{type(response)}' is not type of 'Response'")
256
257     data = list()
258     if response.text.strip() != "":
259         # DEBUG: print(f"DEBUG: response.text()={len(response.text)} is not empty, invoking response.json() ...")
260         try:
261             data = response.json()
262         except json.decoder.JSONDecodeError:
263             pass
264
265     # DEBUG: print(f"DEBUG: data[]='{type(data)}' - EXIT!")
266     return data