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