]> git.mxchange.org Git - fba.git/blob - fba/networks/misskey.py
1ec9cb72b3e67b9508a82ad9ea6fa7ed11f1b724
[fba.git] / fba / networks / misskey.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 logging
19 import validators
20
21 from fba import csrf
22
23 from fba.helpers import blacklist
24 from fba.helpers import config
25 from fba.helpers import dicts
26 from fba.helpers import tidyup
27
28 from fba.http import network
29
30 from fba.models import instances
31
32 logging.basicConfig(level=logging.INFO)
33 logger = logging.getLogger(__name__)
34
35 def fetch_peers(domain: str) -> list:
36     logger.debug("domain(%d)='%s' - CALLED!", len(domain), domain)
37     if not isinstance(domain, str):
38         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
39     elif domain == "":
40         raise ValueError("Parameter 'domain' is empty")
41     elif domain.lower() != domain:
42         raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
43     elif not validators.domain(domain.split("/")[0]):
44         raise ValueError(f"domain='{domain}' is not a valid domain")
45     elif domain.endswith(".arpa"):
46         raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
47     elif domain.endswith(".tld"):
48         raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
49
50     logger.debug(f"domain='{domain}' is misskey, sending API POST request ...")
51     peers   = list()
52     offset  = 0
53     step    = config.get("misskey_limit")
54
55     # No CSRF by default, you don't have to add network.api_headers by yourself here
56     headers = tuple()
57
58     try:
59         logger.debug(f"Checking CSRF for domain='{domain}'")
60         headers = csrf.determine(domain, dict())
61     except network.exceptions as exception:
62         logger.warning(f"Exception '{type(exception)}' during checking CSRF (fetch_peers,{__name__}) - EXIT!")
63         instances.set_last_error(domain, exception)
64         return peers
65
66     # iterating through all "suspended" (follow-only in its terminology)
67     # instances page-by-page, since that troonware doesn't support
68     # sending them all at once
69     while True:
70         logger.debug(f"Fetching offset='{offset}' from '{domain}' ...")
71         if offset == 0:
72             fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
73                 "sort" : "+pubAt",
74                 "host" : None,
75                 "limit": step
76             }), headers)
77         else:
78             fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
79                 "sort"  : "+pubAt",
80                 "host"  : None,
81                 "limit" : step,
82                 "offset": offset - 1
83             }), headers)
84
85         # Check records
86         logger.debug("fetched[]='%s'", type(fetched))
87         if "error_message" in fetched:
88             logger.warning(f"post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
89             instances.set_last_error(domain, fetched)
90             break
91         elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
92             logger.warning(f"post_json_api() returned error: {fetched['error']['message']}")
93             instances.set_last_error(domain, fetched["json"]["error"]["message"])
94             break
95
96         rows = fetched["json"]
97
98         logger.debug(f"rows()={len(rows)}")
99         if len(rows) == 0:
100             logger.debug(f"Returned zero bytes, exiting loop, domain='{domain}'")
101             break
102         elif len(rows) != config.get("misskey_limit"):
103             logger.debug(f"Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
104             offset = offset + (config.get("misskey_limit") - len(rows))
105         else:
106             logger.debug(f"Raising offset by step={step}")
107             offset = offset + step
108
109         already = 0
110         logger.debug(f"rows({len(rows)})[]='{type(rows)}'")
111         for row in rows:
112             logger.debug(f"row()={len(row)}")
113             if "host" not in row:
114                 logger.warning(f"row()={len(row)} does not contain key 'host': {row},domain='{domain}'")
115                 continue
116             elif not isinstance(row["host"], str):
117                 logger.warning(f"row[host][]='{type(row['host'])}' is not 'str' - SKIPPED!")
118                 continue
119             elif not validators.domain(row["host"].split("/")[0]):
120                 logger.warning(f"row[host]='{row['host']}' is not a valid domain - SKIPPED!")
121                 continue
122             elif row["host"].endswith(".arpa"):
123                 logger.warning(f"row[host]='{row['host']}' is a domain for reversed IP addresses - SKIPPED!")
124                 continue
125             elif row["host"].endswith(".tld"):
126                 logger.warning(f"row[host]='{row['host']}' is a fake domain - SKIPPED!")
127                 continue
128             elif blacklist.is_blacklisted(row["host"]):
129                 logger.debug(f"row[host]='{row['host']}' is blacklisted. domain='{domain}' - SKIPPED!")
130                 continue
131             elif row["host"] in peers:
132                 logger.debug(f"Not adding row[host]='{row['host']}', already found.")
133                 already = already + 1
134                 continue
135
136             logger.debug(f"Adding peer: '{row['host']}'")
137             peers.append(row["host"])
138
139         if already == len(rows):
140             logger.debug(f"Host returned same set of '{already}' instances, aborting loop!")
141             break
142
143     logger.debug(f"Adding '{len(peers)}' for domain='{domain}'")
144     instances.set_total_peers(domain, peers)
145
146     logger.debug(f"Returning peers[]='{type(peers)}'")
147     return peers
148
149 def fetch_blocks(domain: str) -> dict:
150     logger.debug("domain(%d)='%s' - CALLED!", len(domain), domain)
151     if not isinstance(domain, str):
152         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
153     elif domain == "":
154         raise ValueError("Parameter 'domain' is empty")
155     elif domain.lower() != domain:
156         raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
157     elif not validators.domain(domain.split("/")[0]):
158         raise ValueError(f"domain='{domain}' is not a valid domain")
159     elif domain.endswith(".arpa"):
160         raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
161     elif domain.endswith(".tld"):
162         raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
163
164     logger.debug(f"Fetching misskey blocks from domain='{domain}'")
165     blocklist = {
166         "suspended": [],
167         "blocked"  : []
168     }
169
170     offset  = 0
171     step    = config.get("misskey_limit")
172
173     # No CSRF by default, you don't have to add network.api_headers by yourself here
174     headers = tuple()
175
176     try:
177         logger.debug(f"Checking CSRF for domain='{domain}'")
178         headers = csrf.determine(domain, dict())
179     except network.exceptions as exception:
180         logger.warning(f"Exception '{type(exception)}' during checking CSRF (fetch_blocks,{__name__}) - EXIT!")
181         instances.set_last_error(domain, exception)
182         return blocklist
183
184     # iterating through all "suspended" (follow-only in its terminology)
185     # instances page-by-page since it doesn't support sending them all at once
186     while True:
187         try:
188             logger.debug(f"Fetching offset='{offset}' from '{domain}' ...")
189             if offset == 0:
190                 logger.debug("Sending JSON API request to domain,step,offset:", domain, step, offset)
191                 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
192                     "sort"     : "+pubAt",
193                     "host"     : None,
194                     "suspended": True,
195                     "limit"    : step
196                 }), headers)
197             else:
198                 logger.debug("Sending JSON API request to domain,step,offset:", domain, step, offset)
199                 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
200                     "sort"     : "+pubAt",
201                     "host"     : None,
202                     "suspended": True,
203                     "limit"    : step,
204                     "offset"   : offset - 1
205                 }), headers)
206
207             logger.debug("fetched[]='%s'", type(fetched))
208             if "error_message" in fetched:
209                 logger.warning(f"post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
210                 instances.set_last_error(domain, fetched)
211                 break
212             elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
213                 logger.warning(f"post_json_api() returned error: {fetched['error']['message']}")
214                 instances.set_last_error(domain, fetched["json"]["error"]["message"])
215                 break
216
217             rows = fetched["json"]
218
219             logger.debug(f"rows({len(rows)})={rows} - suspend")
220             if len(rows) == 0:
221                 logger.debug("Returned zero bytes, exiting loop:", domain)
222                 break
223             elif len(rows) != config.get("misskey_limit"):
224                 logger.debug(f"Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
225                 offset = offset + (config.get("misskey_limit") - len(rows))
226             else:
227                 logger.debug("Raising offset by step:", step)
228                 offset = offset + step
229
230             count = 0
231             for instance in rows:
232                 # Is it there?
233                 logger.debug(f"instance[{type(instance)}]='{instance}' - suspend")
234                 if "isSuspended" in instance and instance["isSuspended"] and not dicts.has_key(blocklist["suspended"], "domain", instance["host"]):
235                     count = count + 1
236                     blocklist["suspended"].append({
237                         "domain": tidyup.domain(instance["host"]),
238                         # no reason field, nothing
239                         "reason": None
240                     })
241
242             logger.debug(f"count={count}")
243             if count == 0:
244                 logger.debug("API is no more returning new instances, aborting loop!")
245                 break
246
247         except network.exceptions as exception:
248             logger.warning(f"Caught error, exiting loop: domain='{domain}',exception[{type(exception)}]='{str(exception)}'")
249             instances.set_last_error(domain, exception)
250             offset = 0
251             break
252
253     while True:
254         # Fetch blocked (full suspended) instances
255         try:
256             if offset == 0:
257                 logger.debug("Sending JSON API request to domain,step,offset:", domain, step, offset)
258                 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
259                     "sort"   : "+pubAt",
260                     "host"   : None,
261                     "blocked": True,
262                     "limit"  : step
263                 }), headers)
264             else:
265                 logger.debug("Sending JSON API request to domain,step,offset:", domain, step, offset)
266                 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
267                     "sort"   : "+pubAt",
268                     "host"   : None,
269                     "blocked": True,
270                     "limit"  : step,
271                     "offset" : offset - 1
272                 }), headers)
273
274             logger.debug("fetched[]='%s'", type(fetched))
275             if "error_message" in fetched:
276                 logger.warning(f"post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
277                 instances.set_last_error(domain, fetched)
278                 break
279             elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
280                 logger.warning(f"post_json_api() returned error: {fetched['error']['message']}")
281                 instances.set_last_error(domain, fetched["json"]["error"]["message"])
282                 break
283
284             rows = fetched["json"]
285
286             logger.debug(f"rows({len(rows)})={rows} - blocked")
287             if len(rows) == 0:
288                 logger.debug("Returned zero bytes, exiting loop:", domain)
289                 break
290             elif len(rows) != config.get("misskey_limit"):
291                 logger.debug(f"Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
292                 offset = offset + (config.get("misskey_limit") - len(rows))
293             else:
294                 logger.debug("Raising offset by step:", step)
295                 offset = offset + step
296
297             count = 0
298             for instance in rows:
299                 # Is it there?
300                 logger.debug(f"instance[{type(instance)}]='{instance}' - blocked")
301                 if "isBlocked" in instance and instance["isBlocked"] and not dicts.has_key(blocklist["blocked"], "domain", instance["host"]):
302                     count = count + 1
303                     blocklist["blocked"].append({
304                         "domain": tidyup.domain(instance["host"]),
305                         "reason": None
306                     })
307
308             logger.debug(f"count={count}")
309             if count == 0:
310                 logger.debug("API is no more returning new instances, aborting loop!")
311                 break
312
313         except network.exceptions as exception:
314             logger.warning(f"Caught error, exiting loop: domain='{domain}',exception[{type(exception)}]='{str(exception)}'")
315             instances.set_last_error(domain, exception)
316             offset = 0
317             break
318
319     logger.debug(f"Returning for domain='{domain}',blocked()={len(blocklist['blocked'])},suspended()={len(blocklist['suspended'])}")
320     return {
321         "reject"        : blocklist["blocked"],
322         "followers_only": blocklist["suspended"]
323     }