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