api.js
1 import { createRestAPIClient, createStreamingAPIClient } from 'masto'; 2 3 import store from './store'; 4 import { 5 getAccount, 6 getAccountByAccessToken, 7 getCurrentAccount, 8 saveAccount, 9 } from './store-utils'; 10 11 // Default *fallback* instance 12 const DEFAULT_INSTANCE = 'mastodon.social'; 13 14 // Per-instance masto instance 15 // Useful when only one account is logged in 16 // I'm not sure if I'll ever allow multiple logged-in accounts but oh well... 17 // E.g. apis['mastodon.social'] 18 const apis = {}; 19 20 // Per-account masto instance 21 // Note: There can be many accounts per instance 22 // Useful when multiple accounts are logged in or when certain actions require a specific account 23 // Just in case if I need this one day. 24 // E.g. accountApis['mastodon.social']['ACCESS_TOKEN'] 25 const accountApis = {}; 26 window.__ACCOUNT_APIS__ = accountApis; 27 28 // Current account masto instance 29 let currentAccountApi; 30 31 export function initClient({ instance, accessToken }) { 32 if (/^https?:\/\//.test(instance)) { 33 instance = instance 34 .replace(/^https?:\/\//, '') 35 .replace(/\/+$/, '') 36 .toLowerCase(); 37 } 38 const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`; 39 40 const masto = createRestAPIClient({ 41 url, 42 accessToken, // Can be null 43 timeout: 30_000, // Unfortunatly this is global instead of per-request 44 }); 45 46 const client = { 47 masto, 48 instance, 49 accessToken, 50 }; 51 apis[instance] = client; 52 if (!accountApis[instance]) accountApis[instance] = {}; 53 if (accessToken) accountApis[instance][accessToken] = client; 54 55 return client; 56 } 57 58 // Get the instance information 59 // The config is needed for composing 60 export async function initInstance(client, instance) { 61 console.log('INIT INSTANCE', client, instance); 62 const { masto, accessToken } = client; 63 // Request v2, fallback to v1 if fail 64 let info; 65 try { 66 info = await masto.v2.instance.fetch(); 67 } catch (e) {} 68 if (!info) { 69 try { 70 info = await masto.v1.instance.fetch(); 71 } catch (e) {} 72 } 73 if (!info) return; 74 console.log(info); 75 const { 76 // v1 77 uri, 78 urls: { streamingApi } = {}, 79 // v2 80 domain, 81 configuration: { urls: { streaming } = {} } = {}, 82 } = info; 83 const instances = store.local.getJSON('instances') || {}; 84 if (uri || domain) { 85 instances[ 86 (domain || uri) 87 .replace(/^https?:\/\//, '') 88 .replace(/\/+$/, '') 89 .toLowerCase() 90 ] = info; 91 } 92 if (instance) { 93 instances[instance.toLowerCase()] = info; 94 } 95 store.local.setJSON('instances', instances); 96 // This is a weird place to put this but here's updating the masto instance with the streaming API URL set in the configuration 97 // Reason: Streaming WebSocket URL may change, unlike the standard API REST URLs 98 const supportsWebSocket = 'WebSocket' in window; 99 if (supportsWebSocket && (streamingApi || streaming)) { 100 console.log('🎏 Streaming API URL:', streaming || streamingApi); 101 // masto.config.props.streamingApiUrl = streaming || streamingApi; 102 // Legacy masto.ws 103 const streamClient = createStreamingAPIClient({ 104 streamingApiUrl: streaming || streamingApi, 105 accessToken, 106 implementation: WebSocket, 107 }); 108 client.streaming = streamClient; 109 // masto.ws = streamClient; 110 console.log('🎏 Streaming API client:', client); 111 } 112 } 113 114 // Get the account information and store it 115 export async function initAccount(client, instance, accessToken, vapidKey) { 116 const { masto } = client; 117 const mastoAccount = await masto.v1.accounts.verifyCredentials(); 118 119 console.log('CURRENTACCOUNT SET', mastoAccount.id); 120 store.session.set('currentAccount', mastoAccount.id); 121 122 saveAccount({ 123 info: mastoAccount, 124 instanceURL: instance.toLowerCase(), 125 accessToken, 126 vapidKey, 127 }); 128 } 129 130 // Get preferences 131 export async function initPreferences(client) { 132 try { 133 const { masto } = client; 134 const preferences = await masto.v1.preferences.fetch(); 135 store.account.set('preferences', preferences); 136 } catch (e) { 137 // silently fail 138 console.error(e); 139 } 140 } 141 142 // Get the masto instance 143 // If accountID is provided, get the masto instance for that account 144 export function api({ instance, accessToken, accountID, account } = {}) { 145 // Always lowercase and trim the instance 146 if (instance) { 147 instance = instance.toLowerCase().trim(); 148 } 149 150 // If instance and accessToken are provided, get the masto instance for that account 151 if (instance && accessToken) { 152 const client = 153 accountApis[instance]?.[accessToken] || 154 initClient({ instance, accessToken }); 155 const { masto, streaming } = client; 156 return { 157 masto, 158 streaming, 159 client, 160 authenticated: true, 161 instance, 162 }; 163 } 164 165 if (accessToken) { 166 // If only accessToken is provided, get the masto instance for that accessToken 167 console.log('X 1', accountApis); 168 for (const instance in accountApis) { 169 if (accountApis[instance][accessToken]) { 170 console.log('X 2', accountApis, instance, accessToken); 171 const client = accountApis[instance][accessToken]; 172 const { masto, streaming } = client; 173 return { 174 masto, 175 streaming, 176 client, 177 authenticated: true, 178 instance, 179 }; 180 } else { 181 console.log('X 3', accountApis, instance, accessToken); 182 const account = getAccountByAccessToken(accessToken); 183 if (account) { 184 const accessToken = account.accessToken; 185 const instance = account.instanceURL.toLowerCase().trim(); 186 const client = initClient({ instance, accessToken }); 187 const { masto, streaming } = client; 188 return { 189 masto, 190 streaming, 191 client, 192 authenticated: true, 193 instance, 194 }; 195 } else { 196 throw new Error(`Access token not found`); 197 } 198 } 199 } 200 } 201 202 // If account is provided, get the masto instance for that account 203 if (account || accountID) { 204 account = account || getAccount(accountID); 205 if (account) { 206 const accessToken = account.accessToken; 207 const instance = account.instanceURL.toLowerCase().trim(); 208 const client = 209 accountApis[instance]?.[accessToken] || 210 initClient({ instance, accessToken }); 211 const { masto, streaming } = client; 212 return { 213 masto, 214 streaming, 215 client, 216 authenticated: true, 217 instance, 218 }; 219 } else { 220 throw new Error(`Account ${accountID} not found`); 221 } 222 } 223 224 // If only instance is provided, get the masto instance for that instance 225 if (instance) { 226 const client = apis[instance] || initClient({ instance }); 227 const { masto, streaming, accessToken } = client; 228 return { 229 masto, 230 streaming, 231 client, 232 authenticated: !!accessToken, 233 instance, 234 }; 235 } 236 237 // If no instance is provided, get the masto instance for the current account 238 if (currentAccountApi) { 239 return { 240 masto: currentAccountApi.masto, 241 streaming: currentAccountApi.streaming, 242 client: currentAccountApi, 243 authenticated: true, 244 instance: currentAccountApi.instance, 245 }; 246 } 247 const currentAccount = getCurrentAccount(); 248 if (currentAccount) { 249 const { accessToken, instanceURL: instance } = currentAccount; 250 currentAccountApi = 251 accountApis[instance]?.[accessToken] || 252 initClient({ instance, accessToken }); 253 return { 254 masto: currentAccountApi.masto, 255 streaming: currentAccountApi.streaming, 256 client: currentAccountApi, 257 authenticated: true, 258 instance, 259 }; 260 } 261 262 // If no instance is provided and no account is logged in, get the masto instance for DEFAULT_INSTANCE 263 const client = 264 apis[DEFAULT_INSTANCE] || initClient({ instance: DEFAULT_INSTANCE }); 265 const { masto, streaming } = client; 266 return { 267 masto, 268 streaming, 269 client, 270 authenticated: false, 271 instance: DEFAULT_INSTANCE, 272 }; 273 } 274 275 window.__API__ = { 276 currentAccountApi, 277 apis, 278 accountApis, 279 };