this repo has no description
at main 393 lines 11 kB view raw
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};