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