]> git.mxchange.org Git - fba.git/blob - fba/networks/misskey.py
239814ceb1a68876d5f7acd9d97be3784492d6d2
[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 from fba import utils
23
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("Checking CSRF for domain='%s'", 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 utils.is_domain_wanted(row["host"]):
120                 logger.debug(f"row[host]='{row['host']}' is not wanted, domain='{domain}' - SKIPPED!")
121                 continue
122             elif row["host"] in peers:
123                 logger.debug(f"Not adding row[host]='{row['host']}', already found.")
124                 already = already + 1
125                 continue
126
127             logger.debug(f"Adding peer: '{row['host']}'")
128             peers.append(row["host"])
129
130         if already == len(rows):
131             logger.debug(f"Host returned same set of '{already}' instances, aborting loop!")
132             break
133
134     logger.debug(f"Adding '{len(peers)}' for domain='{domain}'")
135     instances.set_total_peers(domain, peers)
136
137     logger.debug(f"Returning peers[]='{type(peers)}'")
138     return peers
139
140 def fetch_blocks(domain: str) -> dict:
141     logger.debug("domain(%d)='%s' - CALLED!", len(domain), domain)
142     if not isinstance(domain, str):
143         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
144     elif domain == "":
145         raise ValueError("Parameter 'domain' is empty")
146     elif domain.lower() != domain:
147         raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
148     elif not validators.domain(domain.split("/")[0]):
149         raise ValueError(f"domain='{domain}' is not a valid domain")
150     elif domain.endswith(".arpa"):
151         raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
152     elif domain.endswith(".tld"):
153         raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
154
155     logger.debug(f"Fetching misskey blocks from domain='{domain}'")
156     blocklist = {
157         "suspended": [],
158         "blocked"  : []
159     }
160
161     offset  = 0
162     step    = config.get("misskey_limit")
163
164     # No CSRF by default, you don't have to add network.api_headers by yourself here
165     headers = tuple()
166
167     try:
168         logger.debug("Checking CSRF for domain='%s'", domain)
169         headers = csrf.determine(domain, dict())
170     except network.exceptions as exception:
171         logger.warning(f"Exception '{type(exception)}' during checking CSRF (fetch_blocks,{__name__}) - EXIT!")
172         instances.set_last_error(domain, exception)
173         return blocklist
174
175     # iterating through all "suspended" (follow-only in its terminology)
176     # instances page-by-page since it doesn't support sending them all at once
177     while True:
178         try:
179             logger.debug(f"Fetching offset='{offset}' from '{domain}' ...")
180             if offset == 0:
181                 logger.debug("Sending JSON API request to domain,step,offset:", domain, step, offset)
182                 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
183                     "sort"     : "+pubAt",
184                     "host"     : None,
185                     "suspended": True,
186                     "limit"    : step
187                 }), headers)
188             else:
189                 logger.debug("Sending JSON API request to domain,step,offset:", domain, step, offset)
190                 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
191                     "sort"     : "+pubAt",
192                     "host"     : None,
193                     "suspended": True,
194                     "limit"    : step,
195                     "offset"   : offset - 1
196                 }), headers)
197
198             logger.debug("fetched[]='%s'", type(fetched))
199             if "error_message" in fetched:
200                 logger.warning(f"post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
201                 instances.set_last_error(domain, fetched)
202                 break
203             elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
204                 logger.warning(f"post_json_api() returned error: {fetched['error']['message']}")
205                 instances.set_last_error(domain, fetched["json"]["error"]["message"])
206                 break
207
208             rows = fetched["json"]
209
210             logger.debug(f"rows({len(rows)})={rows} - suspend")
211             if len(rows) == 0:
212                 logger.debug("Returned zero bytes, exiting loop:", domain)
213                 break
214             elif len(rows) != config.get("misskey_limit"):
215                 logger.debug(f"Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
216                 offset = offset + (config.get("misskey_limit") - len(rows))
217             else:
218                 logger.debug("Raising offset by step:", step)
219                 offset = offset + step
220
221             count = 0
222             for instance in rows:
223                 # Is it there?
224                 logger.debug(f"instance[{type(instance)}]='{instance}' - suspend")
225                 if "isSuspended" in instance and instance["isSuspended"] and not dicts.has_key(blocklist["suspended"], "domain", instance["host"]):
226                     count = count + 1
227                     blocklist["suspended"].append({
228                         "domain": tidyup.domain(instance["host"]),
229                         # no reason field, nothing
230                         "reason": None
231                     })
232
233             logger.debug(f"count={count}")
234             if count == 0:
235                 logger.debug("API is no more returning new instances, aborting loop!")
236                 break
237
238         except network.exceptions as exception:
239             logger.warning(f"Caught error, exiting loop: domain='{domain}',exception[{type(exception)}]='{str(exception)}'")
240             instances.set_last_error(domain, exception)
241             offset = 0
242             break
243
244     while True:
245         # Fetch blocked (full suspended) instances
246         try:
247             if offset == 0:
248                 logger.debug("Sending JSON API request to domain,step,offset:", domain, step, offset)
249                 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
250                     "sort"   : "+pubAt",
251                     "host"   : None,
252                     "blocked": True,
253                     "limit"  : step
254                 }), headers)
255             else:
256                 logger.debug("Sending JSON API request to domain,step,offset:", domain, step, offset)
257                 fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
258                     "sort"   : "+pubAt",
259                     "host"   : None,
260                     "blocked": True,
261                     "limit"  : step,
262                     "offset" : offset - 1
263                 }), headers)
264
265             logger.debug("fetched[]='%s'", type(fetched))
266             if "error_message" in fetched:
267                 logger.warning(f"post_json_api() for domain='{domain}' returned error message: {fetched['error_message']}")
268                 instances.set_last_error(domain, fetched)
269                 break
270             elif isinstance(fetched["json"], dict) and "error" in fetched["json"] and "message" in fetched["json"]["error"]:
271                 logger.warning(f"post_json_api() returned error: {fetched['error']['message']}")
272                 instances.set_last_error(domain, fetched["json"]["error"]["message"])
273                 break
274
275             rows = fetched["json"]
276
277             logger.debug(f"rows({len(rows)})={rows} - blocked")
278             if len(rows) == 0:
279                 logger.debug("Returned zero bytes, exiting loop:", domain)
280                 break
281             elif len(rows) != config.get("misskey_limit"):
282                 logger.debug(f"Fetched '{len(rows)}' row(s) but expected: '{config.get('misskey_limit')}'")
283                 offset = offset + (config.get("misskey_limit") - len(rows))
284             else:
285                 logger.debug("Raising offset by step:", step)
286                 offset = offset + step
287
288             count = 0
289             for instance in rows:
290                 # Is it there?
291                 logger.debug(f"instance[{type(instance)}]='{instance}' - blocked")
292                 if "isBlocked" in instance and instance["isBlocked"] and not dicts.has_key(blocklist["blocked"], "domain", instance["host"]):
293                     count = count + 1
294                     blocklist["blocked"].append({
295                         "domain": tidyup.domain(instance["host"]),
296                         "reason": None
297                     })
298
299             logger.debug(f"count={count}")
300             if count == 0:
301                 logger.debug("API is no more returning new instances, aborting loop!")
302                 break
303
304         except network.exceptions as exception:
305             logger.warning(f"Caught error, exiting loop: domain='{domain}',exception[{type(exception)}]='{str(exception)}'")
306             instances.set_last_error(domain, exception)
307             offset = 0
308             break
309
310     logger.debug(f"Returning for domain='{domain}',blocked()={len(blocklist['blocked'])},suspended()={len(blocklist['suspended'])}")
311     return {
312         "reject"        : blocklist["blocked"],
313         "followers_only": blocklist["suspended"]
314     }