this repo has no description
1import { compareVersions, satisfies, validate } from 'compare-versions';
2import { createRestAPIClient, createStreamingAPIClient } from 'masto';
3
4import mem from '../utils/mem';
5
6import store from './store';
7import {
8 getAccount,
9 getAccountByAccessToken,
10 getAccountByInstance,
11 getCurrentAccount,
12 saveAccount,
13 setCurrentAccountID,
14} from './store-utils';
15
16// Default *fallback* instance
17const DEFAULT_INSTANCE = 'mastodon.social';
18
19// Per-instance masto instance
20// Useful when only one account is logged in
21// I'm not sure if I'll ever allow multiple logged-in accounts but oh well...
22// E.g. apis['mastodon.social']
23const apis = {};
24
25// Per-account masto instance
26// Note: There can be many accounts per instance
27// Useful when multiple accounts are logged in or when certain actions require a specific account
28// Just in case if I need this one day.
29// E.g. accountApis['mastodon.social']['ACCESS_TOKEN']
30const accountApis = {};
31window.__ACCOUNT_APIS__ = accountApis;
32
33// Current account masto instance
34let currentAccountApi;
35
36export function initClient({ instance, accessToken }) {
37 if (/^https?:\/\//.test(instance)) {
38 instance = instance
39 .replace(/^https?:\/\//, '')
40 .replace(/\/+$/, '')
41 .toLowerCase();
42 }
43 const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`;
44
45 const masto = createRestAPIClient({
46 url,
47 accessToken, // Can be null
48 timeout: 2 * 60_000, // Unfortunatly this is global instead of per-request
49 });
50
51 const client = {
52 masto,
53 instance,
54 accessToken,
55 };
56 apis[instance] = client;
57 if (!accountApis[instance]) accountApis[instance] = {};
58 if (accessToken) accountApis[instance][accessToken] = client;
59
60 return client;
61}
62
63export function hasInstance(instance) {
64 const instances = store.local.getJSON('instances') || {};
65 return !!instances[instance];
66}
67
68// Get the instance information
69// The config is needed for composing
70export async function initInstance(client, instance) {
71 console.log('INIT INSTANCE', client, instance);
72 const { masto, accessToken } = client;
73 // Request v2, fallback to v1 if fail
74 let info;
75 __BENCHMARK.start('fetch-instance');
76 try {
77 info = await masto.v2.instance.fetch();
78 } catch (e) {}
79 if (!info) {
80 try {
81 info = await masto.v1.instance.fetch();
82 } catch (e) {}
83 }
84 __BENCHMARK.end('fetch-instance');
85 if (!info) return;
86 console.log(info);
87 const {
88 // v1
89 uri,
90 urls: { streamingApi } = {},
91 // v2
92 domain,
93 configuration: { urls: { streaming } = {} } = {},
94 } = info;
95
96 const instances = store.local.getJSON('instances') || {};
97 if (uri || domain) {
98 instances[
99 (domain || uri)
100 .replace(/^https?:\/\//, '')
101 .replace(/\/+$/, '')
102 .toLowerCase()
103 ] = info;
104 }
105 if (instance) {
106 instances[instance.toLowerCase()] = info;
107 }
108 store.local.setJSON('instances', instances);
109
110 let nodeInfo;
111 // GoToSocial requires we get the NodeInfo to identify server type
112 // spec: https://github.com/jhass/nodeinfo
113 try {
114 if (uri || domain) {
115 let urlBase = uri || `https://${domain}`;
116 const wellKnown = await (
117 await fetch(`${urlBase}/.well-known/nodeinfo`)
118 ).json();
119 if (Array.isArray(wellKnown?.links)) {
120 const schema = 'http://nodeinfo.diaspora.software/ns/schema/';
121 const nodeInfoUrl = wellKnown.links
122 .filter(
123 (link) =>
124 typeof link.rel === 'string' &&
125 link.rel.startsWith(schema) &&
126 validate(link.rel.slice(schema.length)),
127 )
128 .map((link) => {
129 let version = link.rel.slice(schema.length);
130 return {
131 version,
132 href: link.href,
133 };
134 })
135 .sort((a, b) => -compareVersions(a.version, b.version))
136 .find((x) => satisfies(x.version, '<=2'))?.href;
137 if (nodeInfoUrl) {
138 nodeInfo = await (await fetch(nodeInfoUrl)).json();
139 }
140 }
141 }
142 } catch (e) {}
143 const nodeInfos = store.local.getJSON('nodeInfos') || {};
144 if (nodeInfo) {
145 nodeInfos[instance.toLowerCase()] = nodeInfo;
146 }
147 store.local.setJSON('nodeInfos', nodeInfos);
148
149 // This is a weird place to put this but here's updating the masto instance with the streaming API URL set in the configuration
150 // Reason: Streaming WebSocket URL may change, unlike the standard API REST URLs
151 const supportsWebSocket = 'WebSocket' in window;
152 if (supportsWebSocket && (streamingApi || streaming)) {
153 console.log('🎏 Streaming API URL:', streaming || streamingApi);
154 // masto.config.props.streamingApiUrl = streaming || streamingApi;
155 // Legacy masto.ws
156 const streamClient = createStreamingAPIClient({
157 streamingApiUrl: streaming || streamingApi,
158 accessToken,
159 implementation: WebSocket,
160 });
161 client.streaming = streamClient;
162 // masto.ws = streamClient;
163 console.log('🎏 Streaming API client:', client);
164 }
165 __BENCHMARK.end('init-instance');
166}
167
168// Get the account information and store it
169export async function initAccount(client, instance, accessToken, vapidKey) {
170 const { masto } = client;
171 const mastoAccount = await masto.v1.accounts.verifyCredentials();
172
173 console.log('CURRENTACCOUNT SET', mastoAccount.id);
174 setCurrentAccountID(mastoAccount.id);
175
176 saveAccount({
177 info: mastoAccount,
178 instanceURL: instance.toLowerCase(),
179 accessToken,
180 vapidKey,
181 createdAt: Date.now(),
182 });
183}
184
185export const getPreferences = mem(
186 () => store.account.get('preferences') || {},
187 {
188 maxAge: 60 * 1000, // 1 minute
189 },
190);
191
192export function setPreferences(preferences) {
193 getPreferences.clear(); // clear memo cache
194 store.account.set('preferences', preferences);
195}
196
197export function hasPreferences() {
198 return !!getPreferences();
199}
200
201// Get preferences
202export async function initPreferences(client) {
203 try {
204 const { masto } = client;
205 __BENCHMARK.start('fetch-preferences');
206 const preferences = await masto.v1.preferences.fetch();
207 __BENCHMARK.end('fetch-preferences');
208 setPreferences(preferences);
209 } catch (e) {
210 // silently fail
211 console.error(e);
212 }
213}
214
215// Get the masto instance
216// If accountID is provided, get the masto instance for that account
217export function api({ instance, accessToken, accountID, account } = {}) {
218 // Always lowercase and trim the instance
219 if (instance) {
220 instance = instance.toLowerCase().trim();
221 }
222
223 // If instance and accessToken are provided, get the masto instance for that account
224 if (instance && accessToken) {
225 const client =
226 accountApis[instance]?.[accessToken] ||
227 initClient({ instance, accessToken });
228 const { masto, streaming } = client;
229 return {
230 masto,
231 streaming,
232 client,
233 authenticated: true,
234 instance,
235 };
236 }
237
238 if (accessToken) {
239 // If only accessToken is provided, get the masto instance for that accessToken
240 console.log('X 1', accountApis);
241 for (const instance in accountApis) {
242 if (accountApis[instance][accessToken]) {
243 console.log('X 2', accountApis, instance, accessToken);
244 const client = accountApis[instance][accessToken];
245 const { masto, streaming } = client;
246 return {
247 masto,
248 streaming,
249 client,
250 authenticated: true,
251 instance,
252 };
253 } else {
254 console.log('X 3', accountApis, instance, accessToken);
255 const account = getAccountByAccessToken(accessToken);
256 if (account) {
257 const accessToken = account.accessToken;
258 const instance = account.instanceURL.toLowerCase().trim();
259 const client = initClient({ instance, accessToken });
260 const { masto, streaming } = client;
261 return {
262 masto,
263 streaming,
264 client,
265 authenticated: true,
266 instance,
267 };
268 } else {
269 throw new Error(`Access token not found`);
270 }
271 }
272 }
273 }
274
275 // If account is provided, get the masto instance for that account
276 if (account || accountID) {
277 account = account || getAccount(accountID);
278 if (account) {
279 const accessToken = account.accessToken;
280 const instance = account.instanceURL.toLowerCase().trim();
281 const client =
282 accountApis[instance]?.[accessToken] ||
283 initClient({ instance, accessToken });
284 const { masto, streaming } = client;
285 return {
286 masto,
287 streaming,
288 client,
289 authenticated: true,
290 instance,
291 };
292 } else {
293 throw new Error(`Account ${accountID} not found`);
294 }
295 }
296
297 const currentAccount = getCurrentAccount();
298
299 // If only instance is provided, get the masto instance for that instance
300 if (instance) {
301 if (currentAccountApi?.instance === instance) {
302 return {
303 masto: currentAccountApi.masto,
304 streaming: currentAccountApi.streaming,
305 client: currentAccountApi,
306 authenticated: true,
307 instance,
308 };
309 }
310
311 if (currentAccount?.instanceURL === instance) {
312 const { accessToken } = currentAccount;
313 currentAccountApi =
314 accountApis[instance]?.[accessToken] ||
315 initClient({ instance, accessToken });
316 return {
317 masto: currentAccountApi.masto,
318 streaming: currentAccountApi.streaming,
319 client: currentAccountApi,
320 authenticated: true,
321 instance,
322 };
323 }
324
325 const instanceAccount = getAccountByInstance(instance);
326 if (instanceAccount) {
327 const accessToken = instanceAccount.accessToken;
328 const client =
329 accountApis[instance]?.[accessToken] ||
330 initClient({ instance, accessToken });
331 const { masto, streaming } = client;
332 return {
333 masto,
334 streaming,
335 client,
336 authenticated: true,
337 instance,
338 };
339 }
340
341 const client = apis[instance] || initClient({ instance });
342 const { masto, streaming, accessToken } = client;
343 return {
344 masto,
345 streaming,
346 client,
347 authenticated: !!accessToken,
348 instance,
349 };
350 }
351
352 // If no instance is provided, get the masto instance for the current account
353 if (currentAccountApi) {
354 return {
355 masto: currentAccountApi.masto,
356 streaming: currentAccountApi.streaming,
357 client: currentAccountApi,
358 authenticated: true,
359 instance: currentAccountApi.instance,
360 };
361 }
362 if (currentAccount) {
363 const { accessToken, instanceURL: instance } = currentAccount;
364 currentAccountApi =
365 accountApis[instance]?.[accessToken] ||
366 initClient({ instance, accessToken });
367 return {
368 masto: currentAccountApi.masto,
369 streaming: currentAccountApi.streaming,
370 client: currentAccountApi,
371 authenticated: true,
372 instance,
373 };
374 }
375
376 // If no instance is provided and no account is logged in, get the masto instance for DEFAULT_INSTANCE
377 const client =
378 apis[DEFAULT_INSTANCE] || initClient({ instance: DEFAULT_INSTANCE });
379 const { masto, streaming } = client;
380 return {
381 masto,
382 streaming,
383 client,
384 authenticated: false,
385 instance: DEFAULT_INSTANCE,
386 };
387}
388
389window.__API__ = {
390 currentAccountApi,
391 apis,
392 accountApis,
393};