From 0c25b35e25e44cff8eb984dce76ffaa16d134abd Mon Sep 17 00:00:00 2001 From: madkarmaa Date: Tue, 1 Apr 2025 10:10:00 +0200 Subject: [PATCH] feat: begin to add API interfaces --- .gitignore | 1 + src/lib/api/interfaces/index.ts | 45 +++++ src/lib/api/models.ts | 133 +++++++++++++ src/lib/api/services/index.ts | 320 ++++++++++++++++++++++++++++++++ src/lib/index.ts | 1 - 5 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 src/lib/api/interfaces/index.ts create mode 100644 src/lib/api/models.ts create mode 100644 src/lib/api/services/index.ts delete mode 100644 src/lib/index.ts diff --git a/.gitignore b/.gitignore index 3b462cb..90abc10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +openapi.json # Output .output diff --git a/src/lib/api/interfaces/index.ts b/src/lib/api/interfaces/index.ts new file mode 100644 index 0000000..9ce1b6d --- /dev/null +++ b/src/lib/api/interfaces/index.ts @@ -0,0 +1,45 @@ +// API service interfaces + +export interface AnnouncementsApi { + getAnnouncements( + cursor?: number, + count?: number, + tag?: string + ): Promise; + getLatestAnnouncement(tag?: string): Promise; + getLatestAnnouncementIds(tag?: string): Promise; + getAnnouncement(id: number): Promise; + createAnnouncement(announcement: ApiAnnouncement, token: string): Promise; + updateAnnouncement(id: number, announcement: ApiAnnouncement, token: string): Promise; + deleteAnnouncement(id: number, token: string): Promise; + getAnnouncementTags(): Promise; +} + +export interface PatchesApi { + getCurrentRelease(prerelease?: boolean): Promise; + getCurrentReleaseVersion(prerelease?: boolean): Promise; + getPatchesList(prerelease?: boolean): Promise; + getPublicKeys(): Promise; +} + +export interface ManagerApi { + getCurrentRelease(prerelease?: boolean): Promise; + getCurrentReleaseVersion(prerelease?: boolean): Promise; +} + +export interface GeneralApi { + getToken(authDigest: string): Promise; + getContributors(): Promise; + getTeamMembers(): Promise; + getAbout(): Promise; + ping(): Promise; + getRateLimit(): Promise; +} + +// unified API interface +export interface RevancedApi { + announcements: AnnouncementsApi; + patches: PatchesApi; + manager: ManagerApi; + general: GeneralApi; +} diff --git a/src/lib/api/models.ts b/src/lib/api/models.ts new file mode 100644 index 0000000..aca1176 --- /dev/null +++ b/src/lib/api/models.ts @@ -0,0 +1,133 @@ +export type BackendResponseAnnouncement = { + archived_at?: Date; + attachments?: string[]; + author?: string; + content?: string; + created_at: Date; + id: number; + tags?: string[]; + title: string; +}; + +export type BackendAnnouncement = { + archived_at?: Date; + attachments?: string[]; + author?: string; + content?: string; + created_at?: Date; + level?: number; + tags?: string[]; + title: string; +}; + +export type BackendLatestPatchesRelease = { + created_at: Date; + description: string; + download_url: string; + signature_download_url?: string; + version: string; +}; + +export type BackendLatestPatchesVersion = { + version: string; +}; + +export type BackendCompatiblePackage = Record; +export type BackendPatchOptionValue = Record; +export type BackendPatchOption = { + title: string; + description: string; + required: boolean; + values?: BackendPatchOptionValue[]; +}; +export type BackendPatch = { + name: string; + description: string; + compatiblePackages: BackendCompatiblePackage; + options: BackendPatchOption[]; +}; + +// TODO + +export type BackendAssetPublicKey = { + patches_public_key: string; +}; + +export type BackendToken = { + token: string; +}; + +export type BackendContributor = { + avatar_url: string; + contributions: number; + name: string; + url: string; +}; + +export type BackendContributable = { + contributors: BackendContributor[]; + name: string; + url: string; +}; + +export type BackendGpgKey = { + id: string; + url: string; +}; + +export type BackendMember = { + avatar_url: string; + bio?: string; + gpg_key?: BackendGpgKey; + name: string; + url: string; +}; + +export type BackendAboutBranding = { + logo: string; +}; + +export type BackendAboutContact = { + email: string; +}; + +export type BackendAboutLink = { + name: string; + preferred?: boolean; + url: string; +}; + +export type BackendAboutWallet = { + address: string; + currency_code: string; + network: string; + preferred?: boolean; +}; + +export type BackendAboutDonations = { + links?: BackendAboutLink[]; + wallets?: BackendAboutWallet[]; +}; + +export type BackendAboutSocial = { + name: string; + preferred?: boolean; + url: string; +}; + +export type BackendAbout = { + about: string; + branding?: BackendAboutBranding; + contact?: BackendAboutContact; + donations?: BackendAboutDonations; + keys: string; + name: string; + socials?: BackendAboutSocial[]; + status: string; +}; + +export type BackendRateLimit = { + limit: number; + remaining: number; + reset: Date; +}; diff --git a/src/lib/api/services/index.ts b/src/lib/api/services/index.ts new file mode 100644 index 0000000..d25cfd8 --- /dev/null +++ b/src/lib/api/services/index.ts @@ -0,0 +1,320 @@ +import type { + AnnouncementsApi, + PatchesApi, + ManagerApi, + GeneralApi, + RevancedApi, + ApiResponseAnnouncement, + ApiAnnouncement, + ApiRelease, + ApiReleaseVersion, + ApiAssetPublicKey, + ApiToken, + APIContributable, + ApiMember, + APIAbout, + ApiRateLimit +} from '../interfaces'; + +// Base URL for the API +const API_BASE_URL = 'https://api.revanced.app'; + +// Helper function to build URLs with query parameters +function buildUrl( + path: string, + params?: Record +): string { + const url = new URL(`${API_BASE_URL}${path}`); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.append(key, String(value)); + } + }); + } + + return url.toString(); +} + +// Implementation of AnnouncementsApi +class RevancedAnnouncementsApi implements AnnouncementsApi { + async getAnnouncements( + cursor?: number, + count?: number, + tag?: string + ): Promise { + const url = buildUrl('/v4/announcements', { cursor, count, tag }); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch announcements: ${response.statusText}`); + } + + return await response.json(); + } + + async getLatestAnnouncement(tag?: string): Promise { + const url = buildUrl('/v4/announcements/latest', { tag }); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch latest announcement: ${response.statusText}`); + } + + return await response.json(); + } + + async getLatestAnnouncementIds(tag?: string): Promise { + const url = buildUrl('/v4/announcements/latest/id', { tag }); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch latest announcement ids: ${response.statusText}`); + } + + return await response.json(); + } + + async getAnnouncement(id: number): Promise { + const url = buildUrl(`/v4/announcements/${id}`); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch announcement: ${response.statusText}`); + } + + return await response.json(); + } + + async createAnnouncement(announcement: ApiAnnouncement, token: string): Promise { + const url = buildUrl('/v4/announcements'); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(announcement) + }); + + if (!response.ok) { + throw new Error(`Failed to create announcement: ${response.statusText}`); + } + } + + async updateAnnouncement( + id: number, + announcement: ApiAnnouncement, + token: string + ): Promise { + const url = buildUrl(`/v4/announcements/${id}`); + const response = await fetch(url, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(announcement) + }); + + if (!response.ok) { + throw new Error(`Failed to update announcement: ${response.statusText}`); + } + } + + async deleteAnnouncement(id: number, token: string): Promise { + const url = buildUrl(`/v4/announcements/${id}`); + const response = await fetch(url, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error(`Failed to delete announcement: ${response.statusText}`); + } + } + + async getAnnouncementTags(): Promise { + const url = buildUrl('/v4/announcements/tags'); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch announcement tags: ${response.statusText}`); + } + + return await response.json(); + } +} + +// Implementation of PatchesApi +class RevancedPatchesApi implements PatchesApi { + async getCurrentRelease(prerelease?: boolean): Promise { + const url = buildUrl('/v4/patches', { prerelease }); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch current patches release: ${response.statusText}`); + } + + return await response.json(); + } + + async getCurrentReleaseVersion(prerelease?: boolean): Promise { + const url = buildUrl('/v4/patches/version', { prerelease }); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch current patches version: ${response.statusText}`); + } + + return await response.json(); + } + + async getPatchesList(prerelease?: boolean): Promise { + const url = buildUrl('/v4/patches/list', { prerelease }); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch patches list: ${response.statusText}`); + } + + return await response.json(); + } + + async getPublicKeys(): Promise { + const url = buildUrl('/v4/patches/keys'); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch patches public keys: ${response.statusText}`); + } + + return await response.json(); + } +} + +// Implementation of ManagerApi +class RevancedManagerApi implements ManagerApi { + async getCurrentRelease(prerelease?: boolean): Promise { + const url = buildUrl('/v4/manager', { prerelease }); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch current manager release: ${response.statusText}`); + } + + return await response.json(); + } + + async getCurrentReleaseVersion(prerelease?: boolean): Promise { + const url = buildUrl('/v4/manager/version', { prerelease }); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch current manager version: ${response.statusText}`); + } + + return await response.json(); + } +} + +// Implementation of GeneralApi +class RevancedGeneralApi implements GeneralApi { + async getToken(authDigest: string): Promise { + const url = buildUrl('/v4/token'); + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: authDigest + } + }); + + if (!response.ok) { + throw new Error(`Failed to get token: ${response.statusText}`); + } + + return await response.json(); + } + + async getContributors(): Promise { + const url = buildUrl('/v4/contributors'); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch contributors: ${response.statusText}`); + } + + return await response.json(); + } + + async getTeamMembers(): Promise { + const url = buildUrl('/v4/team'); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch team members: ${response.statusText}`); + } + + return await response.json(); + } + + async getAbout(): Promise { + const url = buildUrl('/v4/about'); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch about: ${response.statusText}`); + } + + return await response.json(); + } + + async ping(): Promise { + const url = buildUrl('/v4/ping'); + const response = await fetch(url, { method: 'HEAD' }); + return response.ok; + } + + async getRateLimit(): Promise { + const url = buildUrl('/v4/backend/rate_limit'); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch rate limit: ${response.statusText}`); + } + + return await response.json(); + } +} + +// Main implementation of the RevancedApi interface +export class RevancedApiClient implements RevancedApi { + public announcements: AnnouncementsApi; + public patches: PatchesApi; + public manager: ManagerApi; + public general: GeneralApi; + + constructor() { + this.announcements = new RevancedAnnouncementsApi(); + this.patches = new RevancedPatchesApi(); + this.manager = new RevancedManagerApi(); + this.general = new RevancedGeneralApi(); + } +} + +// Create a singleton instance for easy importing +export const revancedApi = new RevancedApiClient(); + +// Example of how to use the API with dependency injection +export async function fetchTeamMembers(api: RevancedApi): Promise { + return await api.general.getTeamMembers(); +} + +// Example usage: +// const teamMembers = await fetchTeamMembers(revancedApi); +// or for testing: +// const mockApi = new MockRevancedApi(); +// const teamMembers = await fetchTeamMembers(mockApi); diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder.