diff --git a/.vscode/settings.json b/.vscode/settings.json index 212a69e..b43fbb5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "conventionalCommits.scopes": ["docs", "build", "federation"] + "conventionalCommits.scopes": ["docs", "build", "federation", "client"] } diff --git a/client/bun.lockb b/client/bun.lockb new file mode 100755 index 0000000..cb3a04f Binary files /dev/null and b/client/bun.lockb differ diff --git a/client/index.ts b/client/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/client/lysand/base.ts b/client/lysand/base.ts new file mode 100644 index 0000000..5660386 --- /dev/null +++ b/client/lysand/base.ts @@ -0,0 +1,191 @@ +import { DEFAULT_UA } from "./constants"; + +type HttpVerb = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; +type ConvertibleObject = Record< + string, + string | number | boolean | File | undefined | null +>; + +export interface Output { + data: ReturnType; + headers: Headers; +} + +const objectToFormData = (obj: ConvertibleObject): FormData => { + return Object.keys(obj).reduce((formData, key) => { + if (obj[key] === undefined || obj[key] === null) return formData; + formData.append(key, String(obj[key])); + return formData; + }, new FormData()); +}; + +export class ResponseError extends Error {} + +export class BaseClient { + constructor( + protected baseUrl: URL, + private accessToken?: string, + ) {} + + get url(): URL { + return this.baseUrl; + } + + get token(): string | undefined { + return this.accessToken; + } + + private async request( + request: Request, + ): Promise> { + const result = await fetch(request); + + if (!result.ok) { + const error = await result.json(); + throw new ResponseError( + `Request failed (${result.status}): ${ + error.error || error.message || result.statusText + }`, + ); + } + + return { + data: await result.json(), + headers: result.headers, + }; + } + + private async constructRequest( + path: string, + method: HttpVerb, + body?: object | FormData, + extra?: RequestInit, + ): Promise { + return new Request(new URL(path, this.baseUrl).toString(), { + method, + headers: { + Authorization: this.accessToken + ? `Bearer ${this.accessToken}` + : "", + "Content-Type": "application/json", + "User-Agent": DEFAULT_UA, + ...extra?.headers, + }, + body: body + ? body instanceof FormData + ? body + : JSON.stringify(body) + : undefined, + ...extra, + }); + } + + protected async get( + path: string, + extra?: RequestInit, + ): Promise> { + return await this.request( + await this.constructRequest(path, "GET", undefined, extra), + ); + } + + protected async post( + path: string, + body?: object, + extra?: RequestInit, + ): Promise> { + return await this.request( + await this.constructRequest(path, "POST", body, extra), + ); + } + + protected async postForm( + path: string, + body: FormData | ConvertibleObject, + extra?: RequestInit, + ): Promise> { + return await this.request( + await this.constructRequest( + path, + "POST", + body instanceof FormData ? body : objectToFormData(body), + extra, + ), + ); + } + + protected async put( + path: string, + body?: object, + extra?: RequestInit, + ): Promise> { + return await this.request( + await this.constructRequest(path, "PUT", body, extra), + ); + } + + protected async putForm( + path: string, + body: FormData | ConvertibleObject, + extra?: RequestInit, + ): Promise> { + return await this.request( + await this.constructRequest( + path, + "PUT", + body instanceof FormData ? body : objectToFormData(body), + extra, + ), + ); + } + + protected async patch( + path: string, + body?: object, + extra?: RequestInit, + ): Promise> { + return await this.request( + await this.constructRequest(path, "PATCH", body, extra), + ); + } + + protected async patchForm( + path: string, + body: FormData | ConvertibleObject, + extra?: RequestInit, + ): Promise> { + return await this.request( + await this.constructRequest( + path, + "PATCH", + body instanceof FormData ? body : objectToFormData(body), + extra, + ), + ); + } + + protected async delete( + path: string, + body?: object, + extra?: RequestInit, + ): Promise> { + return await this.request( + await this.constructRequest(path, "DELETE", body, extra), + ); + } + + protected async deleteForm( + path: string, + body: FormData | ConvertibleObject, + extra?: RequestInit, + ): Promise> { + return await this.request( + await this.constructRequest( + path, + "DELETE", + body instanceof FormData ? body : objectToFormData(body), + extra, + ), + ); + } +} diff --git a/client/lysand/constants.ts b/client/lysand/constants.ts new file mode 100644 index 0000000..1682f60 --- /dev/null +++ b/client/lysand/constants.ts @@ -0,0 +1,5 @@ +import pkg from "../package.json"; + +export const NO_REDIRECT = "urn:ietf:wg:oauth:2.0:oob"; +export const DEFAULT_SCOPE = ["read", "write", "follow"]; +export const DEFAULT_UA = `LysandClient/${pkg.version} (+${pkg.homepage})`; diff --git a/client/lysand/lysand.ts b/client/lysand/lysand.ts new file mode 100644 index 0000000..e770d48 --- /dev/null +++ b/client/lysand/lysand.ts @@ -0,0 +1,1133 @@ +import { OAuth2Client } from "@badgateway/oauth2-client"; +import type { Account } from "../types/account"; +import type { Activity } from "../types/activity"; +import type { Announcement } from "../types/announcement"; +import type { Application, ApplicationData } from "../types/application"; +import type { Attachment } from "../types/attachment"; +import type { Context } from "../types/context"; +import type { Conversation } from "../types/conversation"; +import type { Emoji } from "../types/emoji"; +import type { FeaturedTag } from "../types/featured_tag"; +import type { List } from "../types/list"; +import type { Marker } from "../types/marker"; +import type { Notification } from "../types/notification"; +import type { Poll } from "../types/poll"; +import type { Preferences } from "../types/preferences"; +import type { Relationship } from "../types/relationship"; +import type { ScheduledStatus } from "../types/scheduled_status"; +import type { Status } from "../types/status"; +import type { Tag } from "../types/tag"; +import type { Token } from "../types/token"; +import { BaseClient, type Output } from "./base"; +import { DEFAULT_SCOPE, NO_REDIRECT } from "./constants"; + +type StatusContentType = + | "text/plain" + | "text/markdown" + | "text/html" + | "text/x.misskeymarkdown"; + +interface InstanceV2Output { + domain: string; + title: string; + version: string; + lysand_version: string; + source_url: string; + description: string; + usage: { + users: { + active_month: number; + }; + }; + thumbnail: { + url: string | null; + }; + banner: { + url: string | null; + }; + languages: string[]; + configuration: { + urls: { + streaming: string | null; + status: string | null; + }; + accounts: { + max_featured_tags: number; + }; + statuses: { + max_characters: number; + max_media_attachments: number; + characters_reserved_per_url: number; + }; + media_attachments: { + supported_mime_types: string[]; + image_size_limit: number; + image_matrix_limit: number; + video_size_limit: number; + video_frame_rate_limit: number; + video_matrix_limit: number; + }; + polls: { + max_characters_per_option: number; + max_expiration: number; + max_options: number; + min_expiration: number; + }; + translation: { + enabled: boolean; + }; + }; + registrations: { + enabled: boolean; + approval_required: boolean; + message: string | null; + url: string | null; + }; + contact: { + email: string | null; + account: Account | null; + }; + rules: { + id: string; + text: string; + hint: string; + }[]; + sso: { + forced: boolean; + providers: { + name: string; + icon: string; + id: string; + }[]; + }; +} + +export class LysandClient extends BaseClient { + public acceptFollowRequest( + id: string, + extra?: RequestInit, + ): Promise> { + return this.post( + `/api/v1/follow_requests/${id}/authorize`, + undefined, + extra, + ); + } + + public addAccountToList( + id: string, + account_ids: string[], + extra?: RequestInit, + ): Promise> { + return this.post( + `/api/v1/lists/${id}/accounts`, + { account_ids }, + extra, + ); + } + + public addReactionToAnnouncement( + id: string, + name: string, + extra?: RequestInit, + ): Promise> { + return this.put( + `/api/v1/announcements/${id}/reactions/${name}`, + undefined, + extra, + ); + } + + public blockAccount( + id: string, + extra?: RequestInit, + ): Promise> { + return this.post( + `/api/v1/accounts/${id}/block`, + undefined, + extra, + ); + } + + public blockDomain( + domain: string, + extra?: RequestInit, + ): Promise> { + return this.post("/api/v1/domain_blocks", { domain }, extra); + } + + public bookmarkStatus( + id: string, + extra?: RequestInit, + ): Promise> { + return this.post( + `/api/v1/statuses/${id}/bookmark`, + undefined, + extra, + ); + } + + public cancelScheduledStatus( + id: string, + extra?: RequestInit, + ): Promise> { + return this.delete( + `/api/v1/scheduled_statuses/${id}/cancel`, + undefined, + extra, + ); + } + + public createApp( + client_name: string, + options?: Partial<{ + redirect_uris: string; + scopes: string[]; + website?: string; + }>, + ): Promise> { + return this.postForm("/api/v1/apps", { + client_name, + ...options, + scopes: options?.scopes?.join(" ") || DEFAULT_SCOPE.join(" "), + redirect_uris: options?.redirect_uris || NO_REDIRECT, + }); + } + + public createEmojiReaction( + id: string, + emoji: string, + extra?: RequestInit, + ): Promise> { + return this.post( + `/api/v1/statuses/${id}/reactions/${emoji}`, + undefined, + extra, + ); + } + + public createFeaturedTag( + name: string, + extra?: RequestInit, + ): Promise> { + return this.post("/api/v1/featured_tags", { name }, extra); + } + + public createList( + title: string, + extra?: RequestInit, + ): Promise> { + return this.post("/api/v1/lists", { title }, extra); + } + + public deleteAccountsFromList( + id: string, + account_ids: string[], + extra?: RequestInit, + ): Promise> { + return this.delete( + `/api/v1/lists/${id}/accounts`, + { account_ids }, + extra, + ); + } + + public deleteConversation( + id: string, + extra?: RequestInit, + ): Promise> { + return this.delete( + `/api/v1/conversations/${id}`, + undefined, + extra, + ); + } + + public deleteEmojiReaction( + id: string, + emoji: string, + extra?: RequestInit, + ): Promise> { + return this.delete( + `/api/v1/statuses/${id}/reactions/${emoji}`, + undefined, + extra, + ); + } + + public deleteFeaturedTag( + id: string, + extra?: RequestInit, + ): Promise> { + return this.delete( + `/api/v1/featured_tags/${id}`, + undefined, + extra, + ); + } + + public deleteList(id: string, extra?: RequestInit): Promise> { + return this.delete(`/api/v1/lists/${id}`, undefined, extra); + } + + public deletePushSubscription(extra?: RequestInit): Promise> { + return this.delete("/api/v1/push/subscription", undefined, extra); + } + + public deleteStatus( + id: string, + extra?: RequestInit, + ): Promise> { + return this.delete(`/api/v1/statuses/${id}`, undefined, extra); + } + + // TODO: directStreaming + + public dismissInstanceAnnouncement( + id: string, + extra?: RequestInit, + ): Promise> { + return this.post( + `/api/v1/instance/announcements/${id}/dismiss`, + undefined, + extra, + ); + } + + public dismissNotification( + id: string, + extra?: RequestInit, + ): Promise> { + return this.post( + `/api/v1/notifications/${id}/dismiss`, + undefined, + extra, + ); + } + + public dismissNotifications(extra?: RequestInit): Promise> { + return this.post("/api/v1/notifications/clear", undefined, extra); + } + + public editStatus( + id: string, + options: Partial<{ + status: string; + content_type: StatusContentType; + media_ids: string[]; + poll: Partial<{ + expires_in: number; + hide_totals: boolean; + multiple: boolean; + options: string[]; + }>; + sensitive: boolean; + spoiler_text: string; + language: string; + }>, + extra?: RequestInit, + ): Promise> { + return this.put( + `/api/v1/statuses/${id}`, + { ...options }, + extra, + ); + } + + public favouriteStatus( + id: string, + extra?: RequestInit, + ): Promise> { + return this.post( + `/api/v1/statuses/${id}/favourite`, + undefined, + extra, + ); + } + + public fetchAccessToken( + client_id: string, + client_secret: string, + code?: string, + redirect_uri: string = NO_REDIRECT, + extra?: RequestInit, + ): Promise> { + return this.postForm( + "/oauth/token", + { + client_id, + client_secret, + code, + redirect_uri, + grant_type: "authorization_code", + }, + extra, + ); + } + + public followAccount( + id: string, + extra?: RequestInit, + ): Promise> { + return this.post( + `/api/v1/accounts/${id}/follow`, + undefined, + extra, + ); + } + + public followTag(id: string, extra?: RequestInit): Promise> { + return this.post(`/api/v1/tags/${id}/follow`, undefined, extra); + } + + public generateAuthUrl( + client_id: string, + client_secret: string, + options: Partial<{ + redirect_uri: string; + scopes: string[]; + }>, + ): Promise { + const oauthClient = new OAuth2Client({ + server: this.baseUrl.toString(), + clientId: client_id, + clientSecret: client_secret, + tokenEndpoint: "/oauth/token", + authorizationEndpoint: "/oauth/authorize", + }); + + return oauthClient.authorizationCode.getAuthorizeUri({ + redirectUri: options.redirect_uri || NO_REDIRECT, + scope: options.scopes || DEFAULT_SCOPE, + }); + } + + public getAccount( + id: string, + extra?: RequestInit, + ): Promise> { + return this.get(`/api/v1/accounts/${id}`, extra); + } + + public getAccountFollowers( + id: string, + options?: Partial<{ + max_id: string; + since_id: string; + limit: number; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get( + `/api/v1/accounts/${id}/followers?${params}`, + extra, + ); + } + + public getAccountFollowing( + id: string, + options?: Partial<{ + max_id: string; + since_id: string; + limit: number; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get( + `/api/v1/accounts/${id}/following?${params}`, + extra, + ); + } + + public getAccountLists( + id: string, + extra?: RequestInit, + ): Promise> { + return this.get(`/api/v1/accounts/${id}/lists`, extra); + } + + public getAccountStatuses( + id: string, + options?: Partial<{ + max_id: string; + min_id: string; + since_id: string; + limit: number; + only_media: boolean; + pinned: boolean; + exclude_replies: boolean; + exclude_reblogs: boolean; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + if (options.only_media) params.set("only_media", "true"); + if (options.pinned) params.set("pinned", "true"); + if (options.exclude_replies) params.set("exclude_replies", "true"); + if (options.exclude_reblogs) params.set("exclude_reblogs", "true"); + } + + return this.get( + `/api/v1/accounts/${id}/statuses?${params}`, + extra, + ); + } + + public getAccountsInList( + id: string, + options: Partial<{ + max_id: string; + since_id: string; + limit: number; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get( + `/api/v1/lists/${id}/accounts?${params}`, + extra, + ); + } + + public getBlocks( + options?: Partial<{ + max_id: string; + since_id: string; + limit: number; + }>, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get(`/api/v1/blocks?${params}`); + } + + public getBookmarks( + options?: Partial<{ + max_id: string; + min_id: string; + since_id: string; + limit: number; + }>, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get(`/api/v1/bookmarks?${params}`); + } + + public getConversationTimeline( + id: string, + options?: Partial<{ + max_id: string; + min_id: string; + since_id: string; + limit: number; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get( + `/api/v1/conversations/${id}/timeline?${params}`, + extra, + ); + } + + public getDomainBlocks( + options?: Partial<{ + max_id: string; + since_id: string; + limit: number; + }>, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get(`/api/v1/domain_blocks?${params}`); + } + + public getEndorsements( + options?: Partial<{ + max_id: string; + since_id: string; + limit: number; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get(`/api/v1/endorsements?${params}`, extra); + } + + public getFavourites( + options?: Partial<{ + max_id: string; + min_id: string; + limit: number; + }>, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get(`/api/v1/favourites?${params}`); + } + + public getFeaturedTags( + extra?: RequestInit, + ): Promise> { + return this.get("/api/v1/featured_tags", extra); + } + + // TODO: getFilter + // TODO: getFilters + + public getFollowRequests( + options?: Partial<{ + limit: number; + }>, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get(`/api/v1/follow_requests?${params}`); + } + + public getFollowedTags(extra?: RequestInit): Promise> { + return this.get("/api/v1/followed_tags", extra); + } + + public getHomeTimeline( + options?: Partial<{ + max_id: string; + min_id: string; + since_id: string; + limit: number; + local: boolean; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + if (options.local) params.set("local", "true"); + } + + return this.get(`/api/v1/timelines/home?${params}`, extra); + } + + public getInstance(extra?: RequestInit): Promise> { + return this.get("/api/v2/instance", extra); + } + + public getInstanceActivity( + extra?: RequestInit, + ): Promise> { + return this.get("/api/v1/instance/activity", extra); + } + + public getInstanceAnnouncements( + extra?: RequestInit, + ): Promise> { + return this.get( + "/api/v1/instance/announcements", + extra, + ); + } + + public getInstanceCustomEmojis( + extra?: RequestInit, + ): Promise> { + return this.get("/api/v1/custom_emojis", extra); + } + + public getInstanceDirectory( + options?: Partial<{ + limit: number; + local: boolean; + offset: number; + order: "active" | "new"; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.limit) params.set("limit", options.limit.toString()); + if (options.local) params.set("local", "true"); + if (options.offset) params.set("offset", options.offset.toString()); + if (options.order) params.set("order", options.order); + } + + return this.get(`/api/v1/directory?${params}`, extra); + } + + public getInstancePeers(extra?: RequestInit): Promise> { + return this.get("/api/v1/instance/peers", extra); + } + + public getInstanceTrends( + options?: Partial<{ limit: number }>, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get(`/api/v1/trends?${params}`); + } + + public getList(id: string, extra?: RequestInit): Promise> { + return this.get(`/api/v1/lists/${id}`, extra); + } + + /** + * GET /api/v1/timelines/list/:id + * + * @param id Local ID of the list in the database. + * @param options.limit Max number of results to return. Defaults to 20. + * @param options.max_id Return results older than ID. + * @param options.since_id Return results newer than ID. + * @param options.min_id Return results immediately newer than ID. + * @return Array of statuses. + */ + public getListTimeline( + id: string, + options?: Partial<{ + max_id: string; + min_id: string; + since_id: string; + limit: number; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get( + `/api/v1/timelines/list/${id}?${params}`, + extra, + ); + } + + /** + * GET /api/v1/lists + * + * @return Array of lists. + */ + public getLists(extra?: RequestInit): Promise> { + return this.get("/api/v1/lists", extra); + } + + /** + * GET /api/v1/timelines/public + * + * @param options.only_media Show only statuses with media attached? Defaults to false. + * @param options.limit Max number of results to return. Defaults to 20. + * @param options.max_id Return results older than ID. + * @param options.since_id Return results newer than ID. + * @param options.min_id Return results immediately newer than ID. + * @param options.only_media Show only statuses with media attached? Defaults to false. + * @return Array of statuses. + */ + public getLocalTimeline( + options?: Partial<{ + max_id: string; + min_id: string; + since_id: string; + limit: number; + only_media: boolean; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + if (options.only_media) params.set("only_media", "true"); + } + + return this.get(`/api/v1/timelines/public?${params}`, extra); + } + + /** + * GET /api/v1/markers + * + * @param timelines Array of timeline names, String enum anyOf home, notifications. + * @return Marker or empty object. + */ + public getMarkers( + timelines: ("home" | "notifications")[], + ): Promise>> { + const params = new URLSearchParams(); + + for (const timeline of timelines) { + params.append("timelines[]", timeline); + } + + return this.get>( + `/api/v1/markers?${params}`, + ); + } + + /** + * GET /api/v1/media/:id + * + * @param id Target media ID. + * @return Attachment + */ + public getMedia( + id: string, + extra?: RequestInit, + ): Promise> { + return this.get(`/api/v1/media/${id}`, extra); + } + + /** + * GET /api/v1/mutes + * + * @param options.limit Max number of results to return. Defaults to 40. + * @param options.max_id Return results older than ID. + * @param options.min_id Return results immediately newer than ID. + * @return Array of accounts. + */ + public getMutes( + options?: Partial<{ + max_id: string; + since_id: string; + limit: number; + }>, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get(`/api/v1/mutes?${params}`); + } + + /** + * GET /api/v1/notifications/:id + * + * @param id Target notification ID. + * @return Notification. + */ + public getNotification( + id: string, + extra?: RequestInit, + ): Promise> { + return this.get(`/api/v1/notifications/${id}`, extra); + } + + /** + * GET /api/v1/notifications + * + * @param options.limit Max number of results to return. Defaults to 20. + * @param options.max_id Return results older than ID. + * @param options.since_id Return results newer than ID. + * @param options.min_id Return results immediately newer than ID. + * @param options.exclude_types Array of types to exclude. + * @param options.account_id Return only notifications received from this account. + * @return Array of notifications. + */ + public getNotifications( + options?: Partial<{ + max_id: string; + min_id: string; + since_id: string; + limit: number; + exclude_types: string[]; + account_id: string; + }>, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + if (options.exclude_types) { + for (const type of options.exclude_types) { + params.append("exclude_types[]", type); + } + } + if (options.account_id) + params.set("account_id", options.account_id); + } + + return this.get(`/api/v1/notifications?${params}`); + } + + /** + * GET /api/v1/polls/:id + * + * @param id Target poll ID. + * @return Poll. + */ + public getPoll(id: string, extra?: RequestInit): Promise> { + return this.get(`/api/v1/polls/${id}`, extra); + } + + /** + * GET /api/v1/preferences + * + * @return Preferences. + */ + public getPreferences(extra?: RequestInit): Promise> { + return this.get("/api/v1/preferences", extra); + } + + /** + * GET /api/v1/timelines/public + * + * @param options.only_media Show only statuses with media attached? Defaults to false. + * @param options.limit Max number of results to return. Defaults to 20. + * @param options.max_id Return results older than ID. + * @param options.since_id Return results newer than ID. + * @param options.min_id Return results immediately newer than ID. + * @return Array of statuses. + */ + public getPublicTimeline( + options?: Partial<{ + max_id: string; + min_id: string; + since_id: string; + limit: number; + only_media: boolean; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + if (options.only_media) params.set("only_media", "true"); + } + + return this.get(`/api/v1/timelines/public?${params}`, extra); + } + + /** + * GET /api/v1/push/subscription + * + * @return PushSubscription. + */ + public getPushSubscription( + extra?: RequestInit, + ): Promise> { + return this.get("/api/v1/push/subscription", extra); + } + + /** + * GET /api/v1/accounts/relationships + * + * @param id The account ID. + * @param options.with_suspended Include relationships with suspended accounts? Defaults to false. + * @return Relationship + */ + public getRelationship( + id: string, + options?: Partial<{ + with_suspended: boolean; + }>, + extra?: RequestInit, + ): Promise> { + return this.getRelationships([id], options, extra).then((r) => ({ + data: r.data[0], + headers: r.headers, + })); + } + + /** + * GET /api/v1/accounts/relationships + * + * @param ids Array of account IDs. + * @param options.with_suspended Include relationships with suspended accounts? Defaults to false. + * @return Array of Relationship. + */ + public getRelationships( + ids: string[], + options?: Partial<{ + with_suspended: boolean; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + for (const id of ids) { + params.append("id[]", id); + } + + if (options) { + if (options.with_suspended) params.set("with_suspended", "true"); + } + + return this.get( + `/api/v1/accounts/relationships?${params}`, + extra, + ); + } + + /** + * GET /api/v1/scheduled_statuses/:id + * + * @param id Target status ID. + * @return ScheduledStatus. + */ + public getScheduledStatus( + id: string, + extra?: RequestInit, + ): Promise> { + return this.get( + `/api/v1/scheduled_statuses/${id}`, + extra, + ); + } + + /** + * GET /api/v1/scheduled_statuses + * + * @param options.limit Max number of results to return. Defaults to 20. + * @param options.max_id Return results older than ID. + * @param options.since_id Return results newer than ID. + * @param options.min_id Return results immediately newer than ID. + * @return Array of scheduled statuses. + */ + public getScheduledStatuses( + options?: Partial<{ + max_id: string; + min_id: string; + since_id: string; + limit: number; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.max_id) params.set("max_id", options.max_id); + if (options.min_id) params.set("min_id", options.min_id); + if (options.since_id) params.set("since_id", options.since_id); + if (options.limit) params.set("limit", options.limit.toString()); + } + + return this.get( + `/api/v1/scheduled_statuses?${params}`, + extra, + ); + } + + public getStatus(id: string, extra?: RequestInit): Promise> { + return this.get(`/api/v1/statuses/${id}`, extra); + } + + public getStatusContext( + id: string, + options?: Partial<{ + limit: number; + max_id: string; + since_id: string; + }>, + extra?: RequestInit, + ): Promise> { + const params = new URLSearchParams(); + + if (options) { + if (options.limit) params.set("limit", options.limit.toString()); + if (options.max_id) params.set("max_id", options.max_id); + if (options.since_id) params.set("since_id", options.since_id); + } + + return this.get( + `/api/v1/statuses/${id}/context?${params}`, + extra, + ); + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..ec41472 --- /dev/null +++ b/client/package.json @@ -0,0 +1,60 @@ +{ + "name": "@lysand-org/client", + "displayName": "Lysand Client", + "version": "0.0.0", + "author": { + "email": "jesse.wierzbinski@lysand.org", + "name": "Jesse Wierzbinski (CPlusPatch)", + "url": "https://cpluspatch.com" + }, + "readme": "README.md", + "repository": { + "type": "git", + "url": "https://github.com/lysand-org/api.git", + "directory": "client" + }, + "bugs": { + "url": "https://github.com/lysand-org/api/issues" + }, + "license": "MIT", + "contributors": [ + { + "name": "Jesse Wierzbinski", + "email": "jesse.wierzbinski@lysand.org", + "url": "https://cpluspatch.com" + } + ], + "maintainers": [ + { + "name": "Jesse Wierzbinski", + "email": "jesse.wierzbinski@lysand.org", + "url": "https://cpluspatch.com" + } + ], + "description": "Client for Mastodon and Lysand API", + "categories": ["Other"], + "type": "module", + "engines": { + "bun": ">=1.1.8" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/lysand" + }, + "homepage": "https://lysand.org", + "keywords": ["lysand", "mastodon", "api", "typescript", "rest"], + "packageManager": "bun@1.1.8", + "dependencies": { + "@badgateway/oauth2-client": "^2.3.0", + "@types/mime-types": "^2.1.4", + "magic-regexp": "^0.8.0", + "mime-types": "^2.1.35" + } +} diff --git a/client/types/account.ts b/client/types/account.ts new file mode 100644 index 0000000..9d796c8 --- /dev/null +++ b/client/types/account.ts @@ -0,0 +1,34 @@ +import type { Emoji } from "./emoji"; +import type { Field } from "./field"; +import type { Role } from "./role"; +import type { Source } from "./source"; + +export type Account = { + id: string; + username: string; + acct: string; + display_name: string; + locked: boolean; + discoverable?: boolean; + group: boolean | null; + noindex: boolean | null; + suspended: boolean | null; + limited: boolean | null; + created_at: string; + followers_count: number; + following_count: number; + statuses_count: number; + note: string; + url: string; + avatar: string; + avatar_static: string; + header: string; + header_static: string; + emojis: Array; + moved: Account | null; + fields: Array; + bot: boolean | null; + source?: Source; + role?: Role; + mute_expires_at?: string; +}; diff --git a/client/types/activity.ts b/client/types/activity.ts new file mode 100644 index 0000000..8dbc511 --- /dev/null +++ b/client/types/activity.ts @@ -0,0 +1,6 @@ +export type Activity = { + week: string; + statuses: string; + logins: string; + registrations: string; +}; diff --git a/client/types/announcement.ts b/client/types/announcement.ts new file mode 100644 index 0000000..5a4642c --- /dev/null +++ b/client/types/announcement.ts @@ -0,0 +1,39 @@ +import type { Emoji } from "./emoji"; +import type { StatusTag } from "./status"; + +export type Announcement = { + id: string; + content: string; + starts_at: string | null; + ends_at: string | null; + published: boolean; + all_day: boolean; + published_at: string; + updated_at: string | null; + read: boolean | null; + mentions: Array; + statuses: Array; + tags: Array; + emojis: Array; + reactions: Array; +}; + +export type AnnouncementAccount = { + id: string; + username: string; + url: string; + acct: string; +}; + +export type AnnouncementStatus = { + id: string; + url: string; +}; + +export type AnnouncementReaction = { + name: string; + count: number; + me: boolean | null; + url: string | null; + static_url: string | null; +}; diff --git a/client/types/application.ts b/client/types/application.ts new file mode 100644 index 0000000..caf062d --- /dev/null +++ b/client/types/application.ts @@ -0,0 +1,14 @@ +export type Application = { + name: string; + website?: string | null; + vapid_key?: string | null; +}; + +export type ApplicationData = { + id: string; + name: string; + website?: string | null; + client_id: string; + client_secret: string; + vapid_key?: string | null; +}; diff --git a/client/types/async_attachment.ts b/client/types/async_attachment.ts new file mode 100644 index 0000000..7b2b5b0 --- /dev/null +++ b/client/types/async_attachment.ts @@ -0,0 +1,13 @@ +import type { Meta } from "./attachment"; + +export type AsyncAttachment = { + id: string; + type: "unknown" | "image" | "gifv" | "video" | "audio"; + url: string | null; + remote_url: string | null; + preview_url: string; + text_url: string | null; + meta: Meta | null; + description: string | null; + blurhash: string | null; +}; diff --git a/client/types/attachment.ts b/client/types/attachment.ts new file mode 100644 index 0000000..c3a3697 --- /dev/null +++ b/client/types/attachment.ts @@ -0,0 +1,47 @@ +export type Sub = { + // For Image, Gifv, and Video + width?: number; + height?: number; + size?: string; + aspect?: number; + + // For Gifv and Video + frame_rate?: string; + + // For Audio, Gifv, and Video + duration?: number; + bitrate?: number; +}; + +export type Focus = { + x: number; + y: number; +}; + +export type Meta = { + original?: Sub; + small?: Sub; + focus?: Focus; + length?: string; + duration?: number; + fps?: number; + size?: string; + width?: number; + height?: number; + aspect?: number; + audio_encode?: string; + audio_bitrate?: string; + audio_channel?: string; +}; + +export type Attachment = { + id: string; + type: "unknown" | "image" | "gifv" | "video" | "audio"; + url: string; + remote_url: string | null; + preview_url: string | null; + text_url: string | null; + meta: Meta | null; + description: string | null; + blurhash: string | null; +}; diff --git a/client/types/card.ts b/client/types/card.ts new file mode 100644 index 0000000..6d12122 --- /dev/null +++ b/client/types/card.ts @@ -0,0 +1,16 @@ +export type Card = { + url: string; + title: string; + description: string; + type: "link" | "photo" | "video" | "rich"; + image: string | null; + author_name: string | null; + author_url: string | null; + provider_name: string | null; + provider_url: string | null; + html: string | null; + width: number | null; + height: number | null; + embed_url: string | null; + blurhash: string | null; +}; diff --git a/client/types/context.ts b/client/types/context.ts new file mode 100644 index 0000000..23f9643 --- /dev/null +++ b/client/types/context.ts @@ -0,0 +1,6 @@ +import type { Status } from "./status"; + +export type Context = { + ancestors: Array; + descendants: Array; +}; diff --git a/client/types/conversation.ts b/client/types/conversation.ts new file mode 100644 index 0000000..4a44c9b --- /dev/null +++ b/client/types/conversation.ts @@ -0,0 +1,9 @@ +import type { Account } from "./account"; +import type { Status } from "./status"; + +export type Conversation = { + id: string; + accounts: Array; + last_status: Status | null; + unread: boolean; +}; diff --git a/client/types/emoji.ts b/client/types/emoji.ts new file mode 100644 index 0000000..cc8de67 --- /dev/null +++ b/client/types/emoji.ts @@ -0,0 +1,7 @@ +export type Emoji = { + shortcode: string; + static_url: string; + url: string; + visible_in_picker: boolean; + category?: string; +}; diff --git a/client/types/featured_tag.ts b/client/types/featured_tag.ts new file mode 100644 index 0000000..2fe3ba6 --- /dev/null +++ b/client/types/featured_tag.ts @@ -0,0 +1,6 @@ +export type FeaturedTag = { + id: string; + name: string; + statuses_count: number; + last_status_at: string; +}; diff --git a/client/types/field.ts b/client/types/field.ts new file mode 100644 index 0000000..7798269 --- /dev/null +++ b/client/types/field.ts @@ -0,0 +1,6 @@ +export type Field = { + name: string; + value: string; + verified_at?: string | null; + verified?: boolean | false; +}; diff --git a/client/types/filter.ts b/client/types/filter.ts new file mode 100644 index 0000000..a39928c --- /dev/null +++ b/client/types/filter.ts @@ -0,0 +1,10 @@ +export type Filter = { + id: string; + phrase: string; + context: Array; + expires_at: string | null; + irreversible: boolean; + whole_word: boolean; +}; + +export type FilterContext = string; diff --git a/client/types/follow_request.ts b/client/types/follow_request.ts new file mode 100644 index 0000000..edcc447 --- /dev/null +++ b/client/types/follow_request.ts @@ -0,0 +1,25 @@ +import type { Emoji } from "./emoji"; +import type { Field } from "./field"; + +export type FollowRequest = { + id: number; + username: string; + acct: string; + display_name: string; + locked: boolean; + bot: boolean; + discoverable?: boolean; + group: boolean; + created_at: string; + note: string; + url: string; + avatar: string; + avatar_static: string; + header: string; + header_static: string; + followers_count: number; + following_count: number; + statuses_count: number; + emojis: Array; + fields: Array; +}; diff --git a/client/types/history.ts b/client/types/history.ts new file mode 100644 index 0000000..22176bd --- /dev/null +++ b/client/types/history.ts @@ -0,0 +1,5 @@ +export type History = { + day: string; + uses: number; + accounts: number; +}; diff --git a/client/types/identity_proof.ts b/client/types/identity_proof.ts new file mode 100644 index 0000000..00d9efc --- /dev/null +++ b/client/types/identity_proof.ts @@ -0,0 +1,7 @@ +export type IdentityProof = { + provider: string; + provider_username: string; + updated_at: string; + proof_url: string; + profile_url: string; +}; diff --git a/client/types/instance.ts b/client/types/instance.ts new file mode 100644 index 0000000..b276880 --- /dev/null +++ b/client/types/instance.ts @@ -0,0 +1,38 @@ +import type { Account } from "./account"; +import type { Stats } from "./stats"; +import type { URLs } from "./urls"; + +export type Instance = { + uri: string; + title: string; + description: string; + email: string; + version: string; + thumbnail: string | null; + urls: URLs | null; + stats: Stats; + languages: Array; + registrations: boolean; + approval_required: boolean; + invites_enabled?: boolean; + configuration: { + statuses: { + max_characters: number; + max_media_attachments?: number; + characters_reserved_per_url?: number; + }; + polls?: { + max_options: number; + max_characters_per_option: number; + min_expiration: number; + max_expiration: number; + }; + }; + contact_account?: Account; + rules?: Array; +}; + +export type InstanceRule = { + id: string; + text: string; +}; diff --git a/client/types/list.ts b/client/types/list.ts new file mode 100644 index 0000000..3ae0d37 --- /dev/null +++ b/client/types/list.ts @@ -0,0 +1,7 @@ +export type List = { + id: string; + title: string; + replies_policy: RepliesPolicy | null; +}; + +export type RepliesPolicy = "followed" | "list" | "none"; diff --git a/client/types/marker.ts b/client/types/marker.ts new file mode 100644 index 0000000..d69b0ef --- /dev/null +++ b/client/types/marker.ts @@ -0,0 +1,13 @@ +export type Marker = { + home?: { + last_read_id: string; + version: number; + updated_at: string; + }; + notifications?: { + last_read_id: string; + version: number; + updated_at: string; + unread_count?: number; + }; +}; diff --git a/client/types/mention.ts b/client/types/mention.ts new file mode 100644 index 0000000..dba8929 --- /dev/null +++ b/client/types/mention.ts @@ -0,0 +1,6 @@ +export type Mention = { + id: string; + username: string; + url: string; + acct: string; +}; diff --git a/client/types/notification.ts b/client/types/notification.ts new file mode 100644 index 0000000..d240f49 --- /dev/null +++ b/client/types/notification.ts @@ -0,0 +1,15 @@ +import type { Account } from "./account"; +import type { Reaction } from "./reaction"; +import type { Status } from "./status"; + +export type Notification = { + account: Account | null; + created_at: string; + id: string; + status?: Status; + reaction?: Reaction; + type: NotificationType; + target?: Account; +}; + +export type NotificationType = string; diff --git a/client/types/poll.ts b/client/types/poll.ts new file mode 100644 index 0000000..48fed02 --- /dev/null +++ b/client/types/poll.ts @@ -0,0 +1,14 @@ +export type Poll = { + id: string; + expires_at: string | null; + expired: boolean; + multiple: boolean; + votes_count: number; + options: Array; + voted: boolean; +}; + +export type PollOption = { + title: string; + votes_count: number | null; +}; diff --git a/client/types/preferences.ts b/client/types/preferences.ts new file mode 100644 index 0000000..c0efe96 --- /dev/null +++ b/client/types/preferences.ts @@ -0,0 +1,9 @@ +import type { StatusVisibility } from "./status"; + +export type Preferences = { + "posting:default:visibility": StatusVisibility; + "posting:default:sensitive": boolean; + "posting:default:language": string | null; + "reading:expand:media": "default" | "show_all" | "hide_all"; + "reading:expand:spoilers": boolean; +}; diff --git a/client/types/push_subscription.ts b/client/types/push_subscription.ts new file mode 100644 index 0000000..0449551 --- /dev/null +++ b/client/types/push_subscription.ts @@ -0,0 +1,14 @@ +export type Alerts = { + follow: boolean; + favourite: boolean; + mention: boolean; + reblog: boolean; + poll: boolean; +}; + +export type PushSubscription = { + id: string; + endpoint: string; + server_key: string; + alerts: Alerts; +}; diff --git a/client/types/reaction.ts b/client/types/reaction.ts new file mode 100644 index 0000000..e7fffdf --- /dev/null +++ b/client/types/reaction.ts @@ -0,0 +1,11 @@ +import type { Account } from "./account"; + +export type Reaction = { + count: number; + me: boolean; + name: string; + url?: string; + static_url?: string; + accounts?: Array; + account_ids?: Array; +}; diff --git a/client/types/relationship.ts b/client/types/relationship.ts new file mode 100644 index 0000000..1d5540d --- /dev/null +++ b/client/types/relationship.ts @@ -0,0 +1,15 @@ +export type Relationship = { + id: string; + following: boolean; + followed_by: boolean; + blocking: boolean; + blocked_by: boolean; + muting: boolean; + muting_notifications: boolean; + requested: boolean; + domain_blocking: boolean; + showing_reblogs: boolean; + endorsed: boolean; + notifying: boolean; + note: string | null; +}; diff --git a/client/types/report.ts b/client/types/report.ts new file mode 100644 index 0000000..385e5d8 --- /dev/null +++ b/client/types/report.ts @@ -0,0 +1,16 @@ +import type { Account } from "./account"; + +export type Report = { + id: string; + action_taken: boolean; + action_taken_at: string | null; + status_ids: Array | null; + rule_ids: Array | null; + // These parameters don't exist in Pleroma + category: Category | null; + comment: string | null; + forwarded: boolean | null; + target_account?: Account | null; +}; + +export type Category = "spam" | "violation" | "other"; diff --git a/client/types/results.ts b/client/types/results.ts new file mode 100644 index 0000000..68ffd53 --- /dev/null +++ b/client/types/results.ts @@ -0,0 +1,9 @@ +import type { Account } from "./account"; +import type { Status } from "./status"; +import type { Tag } from "./tag"; + +export type Results = { + accounts: Array; + statuses: Array; + hashtags: Array; +}; diff --git a/client/types/role.ts b/client/types/role.ts new file mode 100644 index 0000000..fdb98ca --- /dev/null +++ b/client/types/role.ts @@ -0,0 +1,3 @@ +export type Role = { + name: string; +}; diff --git a/client/types/scheduled_status.ts b/client/types/scheduled_status.ts new file mode 100644 index 0000000..e8f8a2a --- /dev/null +++ b/client/types/scheduled_status.ts @@ -0,0 +1,9 @@ +import type { Attachment } from "./attachment"; +import type { StatusParams } from "./status_params"; + +export type ScheduledStatus = { + id: string; + scheduled_at: string; + params: StatusParams; + media_attachments: Array | null; +}; diff --git a/client/types/source.ts b/client/types/source.ts new file mode 100644 index 0000000..161be04 --- /dev/null +++ b/client/types/source.ts @@ -0,0 +1,9 @@ +import type { Field } from "./field"; + +export type Source = { + privacy: string | null; + sensitive: boolean | null; + language: string | null; + note: string; + fields: Array; +}; diff --git a/client/types/stats.ts b/client/types/stats.ts new file mode 100644 index 0000000..7654b38 --- /dev/null +++ b/client/types/stats.ts @@ -0,0 +1,5 @@ +export type Stats = { + user_count: number; + status_count: number; + domain_count: number; +}; diff --git a/client/types/status.ts b/client/types/status.ts new file mode 100644 index 0000000..c61c984 --- /dev/null +++ b/client/types/status.ts @@ -0,0 +1,50 @@ +import type { Account } from "./account"; +import type { Application } from "./application"; +import type { Attachment } from "./attachment"; +import type { Card } from "./card"; +import type { Emoji } from "./emoji"; +import type { Mention } from "./mention"; +import type { Poll } from "./poll"; +import type { Reaction } from "./reaction"; + +export type Status = { + id: string; + uri: string; + url: string; + account: Account; + in_reply_to_id: string | null; + in_reply_to_account_id: string | null; + reblog: Status | null; + content: string; + plain_content: string | null; + created_at: string; + edited_at: string | null; + emojis: Emoji[]; + replies_count: number; + reblogs_count: number; + favourites_count: number; + reblogged: boolean | null; + favourited: boolean | null; + muted: boolean | null; + sensitive: boolean; + spoiler_text: string; + visibility: StatusVisibility; + media_attachments: Array; + mentions: Array; + tags: Array; + card: Card | null; + poll: Poll | null; + application: Application | null; + language: string | null; + pinned: boolean | null; + emoji_reactions: Array; + quote: boolean; + bookmarked: boolean; +}; + +export type StatusTag = { + name: string; + url: string; +}; + +export type StatusVisibility = "public" | "unlisted" | "private" | "direct"; diff --git a/client/types/status_params.ts b/client/types/status_params.ts new file mode 100644 index 0000000..89babfb --- /dev/null +++ b/client/types/status_params.ts @@ -0,0 +1,12 @@ +import type { StatusVisibility } from "./status"; + +export type StatusParams = { + text: string; + in_reply_to_id: string | null; + media_ids: Array | null; + sensitive: boolean | null; + spoiler_text: string | null; + visibility: StatusVisibility | null; + scheduled_at: string | null; + application_id: number | null; +}; diff --git a/client/types/status_source.ts b/client/types/status_source.ts new file mode 100644 index 0000000..346c346 --- /dev/null +++ b/client/types/status_source.ts @@ -0,0 +1,5 @@ +export type StatusSource = { + id: string; + text: string; + spoiler_text: string; +}; diff --git a/client/types/tag.ts b/client/types/tag.ts new file mode 100644 index 0000000..da41dd3 --- /dev/null +++ b/client/types/tag.ts @@ -0,0 +1,8 @@ +import type { History } from "./history"; + +export type Tag = { + name: string; + url: string; + history: Array; + following?: boolean; +}; diff --git a/client/types/token.ts b/client/types/token.ts new file mode 100644 index 0000000..0021a49 --- /dev/null +++ b/client/types/token.ts @@ -0,0 +1,6 @@ +export type Token = { + access_token: string; + token_type: string; + scope: string; + created_at: number; +}; diff --git a/client/types/urls.ts b/client/types/urls.ts new file mode 100644 index 0000000..59cca70 --- /dev/null +++ b/client/types/urls.ts @@ -0,0 +1,3 @@ +export type URLs = { + streaming_api: string; +}; diff --git a/tsconfig.json b/tsconfig.json index c8aa060..1cf67a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,9 @@ "allowJs": true, "emitDecoratorMetadata": false, "experimentalDecorators": true, - "verbatimModuleSyntax": true + "verbatimModuleSyntax": true, + "noUnusedLocals": true, + "noUnusedParameters": true }, "include": ["*.ts", "*.d.ts", "**/*.ts", "**/*.d.ts"] }