/ src / utils / api.js
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  };