feat: begin to add API interfaces

This commit is contained in:
madkarmaa 2025-04-01 10:10:00 +02:00
parent f0c2ff64ed
commit 0c25b35e25
5 changed files with 499 additions and 1 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules
openapi.json
# Output
.output

View File

@ -0,0 +1,45 @@
// API service interfaces
export interface AnnouncementsApi {
getAnnouncements(
cursor?: number,
count?: number,
tag?: string
): Promise<ApiResponseAnnouncement[]>;
getLatestAnnouncement(tag?: string): Promise<ApiResponseAnnouncement[]>;
getLatestAnnouncementIds(tag?: string): Promise<ApiResponseAnnouncement[]>;
getAnnouncement(id: number): Promise<ApiResponseAnnouncement>;
createAnnouncement(announcement: ApiAnnouncement, token: string): Promise<void>;
updateAnnouncement(id: number, announcement: ApiAnnouncement, token: string): Promise<void>;
deleteAnnouncement(id: number, token: string): Promise<void>;
getAnnouncementTags(): Promise<string[]>;
}
export interface PatchesApi {
getCurrentRelease(prerelease?: boolean): Promise<ApiRelease>;
getCurrentReleaseVersion(prerelease?: boolean): Promise<ApiReleaseVersion>;
getPatchesList(prerelease?: boolean): Promise<string>;
getPublicKeys(): Promise<ApiAssetPublicKey>;
}
export interface ManagerApi {
getCurrentRelease(prerelease?: boolean): Promise<ApiRelease>;
getCurrentReleaseVersion(prerelease?: boolean): Promise<ApiReleaseVersion>;
}
export interface GeneralApi {
getToken(authDigest: string): Promise<ApiToken>;
getContributors(): Promise<APIContributable[]>;
getTeamMembers(): Promise<ApiMember[]>;
getAbout(): Promise<APIAbout>;
ping(): Promise<boolean>;
getRateLimit(): Promise<ApiRateLimit>;
}
// unified API interface
export interface RevancedApi {
announcements: AnnouncementsApi;
patches: PatchesApi;
manager: ManagerApi;
general: GeneralApi;
}

133
src/lib/api/models.ts Normal file
View File

@ -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<string, string[]>;
export type BackendPatchOptionValue = Record<string, string | number>;
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;
};

View File

@ -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, string | number | boolean | undefined>
): 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<ApiResponseAnnouncement[]> {
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<ApiResponseAnnouncement[]> {
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<ApiResponseAnnouncement[]> {
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<ApiResponseAnnouncement> {
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<void> {
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<void> {
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<void> {
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<string[]> {
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<ApiRelease> {
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<ApiReleaseVersion> {
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<string> {
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<ApiAssetPublicKey> {
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<ApiRelease> {
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<ApiReleaseVersion> {
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<ApiToken> {
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<APIContributable[]> {
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<ApiMember[]> {
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<APIAbout> {
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<boolean> {
const url = buildUrl('/v4/ping');
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
}
async getRateLimit(): Promise<ApiRateLimit> {
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<ApiMember[]> {
return await api.general.getTeamMembers();
}
// Example usage:
// const teamMembers = await fetchTeamMembers(revancedApi);
// or for testing:
// const mockApi = new MockRevancedApi();
// const teamMembers = await fetchTeamMembers(mockApi);

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.