From 5047dada7c263c8fb9261f5de018274c59f5b6de Mon Sep 17 00:00:00 2001 From: Guilherme Werner Date: Sun, 31 Dec 2023 23:43:01 -0300 Subject: [PATCH] Improve api - TribufuApi - TribufuBot - TribufuClient - TribufuServer --- src/api.ts | 443 ++++++++++++++++++++++++++++++++++++ src/bot.ts | 40 ++++ src/client.ts | 326 +++++++++++++++++++++++++++ src/constants.js | 21 ++ src/http.ts | 17 ++ src/index.ts | 542 +-------------------------------------------- src/models/user.ts | 26 +++ src/node.ts | 7 + src/oauth2.ts | 7 +- src/options.ts | 8 + src/server.ts | 57 +++++ tsconfig.json | 2 + 12 files changed, 961 insertions(+), 535 deletions(-) create mode 100644 src/api.ts create mode 100644 src/bot.ts create mode 100644 src/client.ts create mode 100644 src/constants.js create mode 100644 src/http.ts create mode 100644 src/models/user.ts create mode 100644 src/node.ts create mode 100644 src/options.ts create mode 100644 src/server.ts diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..8ef643a --- /dev/null +++ b/src/api.ts @@ -0,0 +1,443 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +import { HeaderMap } from "./http"; +import { JavaScriptRuntime } from "./node"; +import { TribufuApiOptions } from "./options"; +import { TribufuClient } from "./client"; +import { TribufuServer } from "./server"; +import axios, { AxiosInstance } from "axios"; + +export const TRIBUFU_VERSION = "0.0.0"; +export const TRIBUFU_API_URL = "https://api.tribufu.com"; + +/** + * **Tribufu API** + * + * Use this class to interact with the Tribufu API. + * + * *There are three ways to use the Tribufu API:* + * - A api key give you public read only access to the Tribufu API. + * - A bot give you read and write access to the Tribufu API as a bot account. + * - A client give you read and write access to the Tribufu API as a client application. + */ +export class TribufuApi { + protected readonly http: AxiosInstance; + protected readonly options: TribufuApiOptions; + + constructor(options?: TribufuApiOptions | null) { + this.options = options || {}; + + let http = axios.create({ + baseURL: TribufuApi.getBaseUrl(), + headers: TribufuApi.defaultHeaders(), + }); + + if (TribufuApi.debugEnabled()) { + http.interceptors.request.use((req) => { + console.log(`(TribufuApi) ${req.method?.toUpperCase()} ${req.baseURL}${req.url}`); + return req; + }); + } + + this.http = http; + } + + public static default(): TribufuApi { + return new TribufuApi({}); + } + + /** + * Create a TribufuApi with the given api key. + * + * - A api key give you public read only access to the Tribufu API. + * + * @param apiKey + * @returns TribufuApi + */ + public static withApiKey(apiKey: string): TribufuApi { + return new TribufuApi({ apiKey }); + } + + /** + * Create a TribufuBot with the given bot token. + * + * - A bot give you read and write access to the Tribufu API as a bot account. + * + * @param token + * @returns TribufuBot + */ + public static withBot(token: string): TribufuBot { + return new TribufuBot(token); + } + + /** + * Create a TribufuClient with the given client id and client secret. + * + * @param clientId + * @param clientSecret + * @returns TribufuClient + */ + public static withClient(clientId: string, clientSecret: string): TribufuClient { + return new TribufuClient(clientId, clientSecret); + } + + /** + * Create a TribufuServer with the given server id, client id and client secret. + * @param serverId + * @param clientId + * @param clientSecret + * @returns TribufuServer + */ + public static withServer(serverId: string, clientId: string, clientSecret: string): TribufuServer { + return new TribufuServer(serverId, clientId, clientSecret); + } + + /** + * Try to create a TribufuApi from environment variables. + * + * - This will only work if the environment variables are set. + * + * @param prefix A prefix for the environment variables. + * @returns TribufuApi | null + * @example + * ```ts + * // process.env.TRIBUFU_API_KEY + * const api = TribufuApi.fromEnv("TRIBUFU_"); + * ``` + */ + public static fromEnv(prefix: string = ""): TribufuApi | null { + const apiKey = process.env[`${prefix}API_KEY`]; + + if (apiKey) { + return TribufuApi.withApiKey(apiKey); + } + + return null; + } + + /** + * Create a TribufuApi from environment variables or the default api. + * + * - This will fallback to the default api if the environment variables are not set. + * + * @param prefix A prefix for the environment variables. + * @returns TribufuApi | null + * @example + * ```ts + * // process.env.TRIBUFU_API_KEY = null + * const api = TribufuApi.fromEnvOrDefault("TRIBUFU_"); + * ``` + */ + public static fromEnvOrDefault(prefix: string = ""): TribufuApi { + return TribufuApi.fromEnv(prefix) || TribufuApi.default(); + } + + /** + * Check if debug mode is enabled. + * + * - Debug mode is enabled if the environment variable `NODE_ENV` is set to `development`. + * - Debug mode is disabled by default. + * - Debug mode is disabled in the browser. + * + * @returns boolean + */ + private static debugEnabled(): boolean { + return process.env.NODE_ENV ? process.env.NODE_ENV === "development" : false; + } + + /** + * Get the base url for the Tribufu API. + * + * - The base url can be set using the environment variable `TRIBUFU_API_URL`. + * - The custom base url is only used if debug mode is enabled. + * - The default base url is `https://api.tribufu.com`. + * + * @returns string + */ + private static getBaseUrl(): string { + const baseUrl = process.env[`TRIBUFU_API_URL`] || null; + return TribufuApi.debugEnabled() && baseUrl ? baseUrl : TRIBUFU_API_URL; + } + + /** + * Get the default headers for the Tribufu API. + * @returns HeaderMap + */ + private static defaultHeaders(): HeaderMap { + const headers = { + "X-Tribufu-Language": "javascript", + "X-Tribufu-Version": TRIBUFU_VERSION, + }; + + return headers; + } + + /** + * Detect the current JavaScript runtime. + * + * - This is used to determine if the code is running in a browser or in Node.js. + * + * @returns JavaScriptRuntime + */ + private static detectRuntime(): JavaScriptRuntime { + if (typeof window !== "undefined") { + return JavaScriptRuntime.Browser; + } + + if (typeof process !== "undefined" && process?.versions?.node) { + return JavaScriptRuntime.Node; + } + + return JavaScriptRuntime.Other; + } + + /** + * Check if the current JavaScript runtime is a browser. + * @returns boolean + */ + public static isBrowser(): boolean { + return TribufuApi.detectRuntime() === JavaScriptRuntime.Browser; + } + + /** + * Check if the current JavaScript runtime is Node.js. + * @returns boolean + */ + public static isNode(): boolean { + return TribufuApi.detectRuntime() === JavaScriptRuntime.Node; + } + + /** + * Get current headers with the api key or access token. + * @returns HeaderMap + */ + protected getHeaders(): HeaderMap { + let headers: HeaderMap = {}; + + if (this.options.apiKey) { + headers["Authorization"] = `ApiKey ${this.options.apiKey}`; + return headers; + } + + if (this.options.accessToken) { + headers["Authorization"] = `Bearer ${this.options.accessToken}`; + return headers; + } + + return headers; + } + + /** + * Get a resource from the Tribufu API. + * @returns T | null + */ + protected async get(path: string, headers?: HeaderMap | null): Promise { + try { + const requestHeaders = headers || this.getHeaders(); + const response = await this.http.get(path, { headers: requestHeaders }); + + if (response.status !== 200) { + return null; + } + + return response.data as T; + } catch (error) { + return null; + } + } + + /** + * Create a resource to the Tribufu API. + * @param path + * @param body + * @param headers + * @returns T | null + */ + protected async post(path: string, body: S, headers?: HeaderMap | null): Promise { + try { + const requestHeaders = headers || this.getHeaders(); + const response = await this.http.post(path, body, { headers: requestHeaders }); + + if (response.status !== 200) { + return null; + } + + return response.data as T; + } catch (error) { + return null; + } + } + + /** + * Update a resource on the Tribufu API. + * @param path + * @param body + * @param headers + * @returns T | null + */ + protected async put(path: string, body: S, headers?: HeaderMap | null): Promise { + try { + const requestHeaders = headers || this.getHeaders(); + const response = await this.http.put(path, body, { headers: requestHeaders }); + + if (response.status !== 200) { + return null; + } + + return response.data as T; + } catch (error) { + return null; + } + } + + /** + * Delete a resource from the Tribufu API. + * @param path + * @param headers + * @returns T | null + */ + protected async delete(path: string, headers?: HeaderMap | null): Promise { + try { + const requestHeaders = headers || this.getHeaders(); + const response = await this.http.delete(path, { headers: requestHeaders }); + + if (response.status !== 200) { + return null; + } + + return response.data as T; + } catch (error) { + return null; + } + } + + /** + * Get games from the Tribufu API. + * @param page + * @returns Game[] + */ + public async getGames(page: number = 1): Promise { + const headers = this.getHeaders(); + const responseBody = await this.get(`/v1/packages?page=${page}`, headers); + + if (!responseBody) { + return []; + } + + return responseBody; + } + + /** + * Get a game from the Tribufu API. + * @param id + * @returns Game | null + */ + public async getGameyId(id: string): Promise { + const headers = this.getHeaders(); + const responseBody = await this.get(`/v1/packages/${id}`, headers); + + if (!responseBody) { + return null; + } + + return responseBody; + } + + /** + * Get a game from the Tribufu API. + * @param page + * @returns Server[] + */ + public async getServers(page: number = 1): Promise { + const headers = this.getHeaders(); + const responseBody = await this.get(`/v1/servers?page=${page}`, headers); + + if (!responseBody) { + return []; + } + + return responseBody; + } + + /** + * Get a server by id or address from the Tribufu API. + * @param idOrAddress + * @returns Server | null + */ + public async getServer(idOrAddress: string): Promise { + if (isNaN(idOrAddress as any)) { + return await this.getServerByAddress(idOrAddress); + } + + return await this.getServerById(idOrAddress); + } + + /** + * Get a server by id from the Tribufu API. + * @param id + * @returns Server | null + */ + public async getServerById(id: string): Promise { + const headers = this.getHeaders() + const responseBody = await this.get(`/v1/servers/${id}`, headers); + + if (!responseBody) { + return null; + } + + return responseBody; + } + + /** + * Get a server by address from the Tribufu API. + * @param address + * @returns Server | null + */ + public async getServerByAddress(address: string): Promise { + const headers = this.getHeaders(); + const responseBody = await this.get(`/v1/servers/address/${address}`, headers); + + if (!responseBody) { + return null; + } + + return responseBody; + } + +} + +/** + * **Tribufu Bot** + * + * To authenticate a bot you need to use the bot token obtained from the Tribufu Developer Portal. + * + * - A bot is a special type of user account. + * - A bot give you read and write access to the Tribufu API. + */ +export class TribufuBot extends TribufuApi { + constructor(token: string) { + super({ accessToken: token }); + } + + /** + * Try to create a TribufuBot from environment variables. + * + * - This will only work if the environment variables are set. + * + * @param prefix A prefix for the environment variables. + * @returns TribufuBot | null + * @example + * ```ts + * const bot = TribufuBot.fromEnv("TRIBUFU_"); // process.env.TRIBUFU_BOT_TOKEN + * ``` + */ + public static override fromEnv(prefix: string = ""): TribufuBot | null { + const token = process.env[`${prefix}BOT_TOKEN`]; + + if (token) { + return TribufuApi.withBot(token); + } + + return null; + } +} diff --git a/src/bot.ts b/src/bot.ts new file mode 100644 index 0000000..53fe3d7 --- /dev/null +++ b/src/bot.ts @@ -0,0 +1,40 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +import { TribufuApi } from "./api"; + +/** + * **Tribufu Bot** + * + * To authenticate a bot you need to use the bot token obtained from the Tribufu Developer Portal. + * + * - A bot is a special type of user account. + * - A bot give you read and write access to the Tribufu API. + */ +export class TribufuBot extends TribufuApi { + constructor(token: string) { + super({ accessToken: token }); + } + + /** + * Try to create a TribufuBot from environment variables. + * + * - This will only work if the environment variables are set. + * + * @param prefix A prefix for the environment variables. + * @returns TribufuBot | null + * @example + * ```ts + * // process.env.TRIBUFU_BOT_TOKEN + * const bot = TribufuBot.fromEnv("TRIBUFU_"); + * ``` + */ + public static override fromEnv(prefix: string = ""): TribufuBot | null { + const token = process.env[`${prefix}BOT_TOKEN`]; + + if (token) { + return TribufuApi.withBot(token); + } + + return null; + } +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..f242383 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,326 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +import { CookieMap, HeaderMap } from "./http"; +import { OAuth2GrantType, OAuth2IntrospectionRequest, OAuth2IntrospectionResponse, OAuth2TokenRequest, OAuth2TokenResponse } from "./oauth2"; +import { TribufuApi } from "./api"; +import { User } from "./models/user"; + +/** + * **Tribufu Client** + * + * To authenticate a client you need to use the client id and client secret obtained from the Tribufu Developer Portal + * + * - A client is how external applications interact with the Tribufu API. + * - A client give you read and write access to the Tribufu API. + * - A client can be used to login users. + */ +export class TribufuClient extends TribufuApi { + private readonly clientId: string; + private readonly clientSecret: string; + + constructor(clientId: string, clientSecret: string) { + super({}); + + this.clientId = clientId; + this.clientSecret = clientSecret; + } + + /** + * Try to create a TribufuClient from environment variables. + * + * - This will only work if the environment variables are set. + * + * @param prefix A prefix for the environment variables. + * @returns TribufuClient | null + * @example + * ```ts + * // process.env.TRIBUFU_CLIENT_ID + * // process.env.TRIBUFU_CLIENT_SECRET + * const client = TribufuClient.fromEnv("TRIBUFU_"); + * ``` + */ + public static override fromEnv(prefix: string = ""): TribufuClient | null { + const clientId = process.env[`${prefix}CLIENT_ID`]; + const clientSecret = process.env[`${prefix}CLIENT_SECRET`]; + + if (clientId && clientSecret) { + return TribufuApi.withClient(clientId, clientSecret); + } + + return null; + } + + /** + * Try to create a TribufuClient from environment variables and cookies. + * + * - This will only work if the environment variables are set. + * - The cookies are used to get the access token and refresh token. + * + * @param cookies Cookies from the request. + * @param prefix A prefix for the environment variables. + * @returns TribufuClient | null + * @example + * ```ts + * // process.env.TRIBUFU_CLIENT_ID + * // process.env.TRIBUFU_CLIENT_SECRET + * const cookies = { "access_token": "...", "refresh_token": "..." }; + * const client = TribufuClient.fromEnvAndCookies(cookies, "TRIBUFU_"); + * ``` + */ + public static fromEnvAndCookies(cookies: CookieMap, prefix: string = ""): TribufuClient | null { + const client = TribufuClient.fromEnv(prefix); + const accessToken = cookies["access_token"] || null; + const refreshToken = cookies["refresh_token"] || null; + + if (client) { + client.setTokens(accessToken, refreshToken); + } + + return client; + } + + private setTokens(accessToken: string | null, refreshToken?: string | null, expiresIn?: number | null): void { + this.options.accessToken = accessToken; + this.options.refreshToken = refreshToken || null; + this.options.expiresIn = expiresIn || null; + } + + private clearTokens(): void { + this.setTokens(null, null, null); + } + + private setTokensFromResponse(tokens: OAuth2TokenResponse): void { + this.setTokens(tokens.access_token, tokens.refresh_token || null, tokens.expires_in || null); + } + + private getOAuthHeaders(): HeaderMap { + let headers = this.getHeaders(); + headers["Authorization"] = `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`, "binary").toString("base64")}`; + headers["Content-Type"] = "application/x-www-form-urlencoded"; + return headers; + } + + /** + * Login using an authorization code. + * @param authorizationCode + * @returns + */ + public async authorizationLogin(authorizationCode: string): Promise { + const response = await this.getToken("authorization_code", authorizationCode, null, null); + + if (!response) { + return false; + } + + this.setTokensFromResponse(response); + + return true; + } + + /** + * Login using a device code. + * @param deviceCode + * @returns + */ + public async deviceLogin(deviceCode: string): Promise { + const response = await this.getToken("device_code", deviceCode, null, null); + + if (!response) { + return false; + } + + this.setTokensFromResponse(response); + + return true; + } + + /** + * Login using a username and password. + * @param username + * @param password + * @returns + */ + public async passwordLogin(username: string, password: string): Promise { + const response = await this.getToken("password", password, null, username); + + if (!response) { + return false; + } + + this.setTokensFromResponse(response); + + return true; + } + + /** + * Login using a passkey. + * @param username + * @param passkey + * @returns + */ + public async passkeyLogin(username: string, passkey: string): Promise { + const response = await this.getToken("passkey", passkey, null, username); + + if (!response) { + return false; + } + + this.setTokensFromResponse(response); + + return true; + } + + /** + * Get a token for a client application. + * @param subjectKey + * @param subjectValue + * @returns + */ + public async clientLogin(subjectKey: string | null, subjectValue: string | null): Promise { + const response = await this.getToken("client_credentials", null, subjectKey, subjectValue); + + if (!response) { + return false; + } + + this.setTokensFromResponse(response); + + return true; + } + + /** + * Get a new access token using a refresh token. + * @param refreshToken + * @returns + */ + public async refreshToken(): Promise { + if (!this.options.refreshToken) { + return false; + } + + const response = await this.getToken("refresh_token", this.options.refreshToken, null, null); + + if (!response) { + return false; + } + + this.setTokensFromResponse(response); + + return true; + } + + /** + * Get information about a access token. + * @returns + */ + public async instrospectToken(): Promise { + if (!this.options.accessToken) { + return null; + } + + const requestBody: OAuth2IntrospectionRequest = { + token: this.options.accessToken, + token_type_hint: "access_token", + } + + const url = `/v1/oauth2/introspect`; + const headers = this.getOAuthHeaders(); + const responseBody = await this.post(url, requestBody, headers); + + if (!responseBody) { + return null; + } + + return responseBody; + } + + /** + * Revoke a refresh token. + * @returns + */ + public async revokeToken(): Promise { + if (!this.options.refreshToken) { + return false; + } + + const requestBody: OAuth2IntrospectionRequest = { + token: this.options.refreshToken, + token_type_hint: "refresh_token", + } + + const url = `/v1/oauth2/revoke`; + const headers = this.getOAuthHeaders(); + const responseBody = await this.post(url, requestBody, headers); + + if (!responseBody) { + return false; + } + + this.clearTokens(); + + return true; + } + + public async isTokenValid(): Promise { + const response = await this.instrospectToken(); + + if (!response) { + return false; + } + + return response.active; + } + + /** + * Get a oauth2 token with the given grant type. + * @param grantType + * @param grantValue + * @param subjectKey + * @param subjectValue + * @returns + */ + protected async getToken(grantType: OAuth2GrantType, grantValue: string | null, subjectKey: string | null, subjectValue: string | null): Promise { + const requestBody: OAuth2TokenRequest = { + grant_type: grantType, + code: grantType === "authorization_code" ? grantValue : null, + refresh_token: grantType === "refresh_token" ? grantValue : null, + username: grantType === "password" || grantType === "passkey" ? subjectValue : null, + password: grantType === "password" ? grantValue : null, + passkey: grantType === "passkey" ? grantValue : null, + client_id: this.clientId, + client_secret: this.clientSecret, + } + + const params = subjectKey && subjectValue ? `?${subjectKey}=${subjectValue}` : ""; + const url = `/v1/oauth2/token${params}`; + const headers = this.getOAuthHeaders(); + const responseBody = await this.post(url, requestBody, headers); + + if (!responseBody) { + return null; + } + + return responseBody; + } + + /** + * Get information about the current user. + * @returns + */ + public async getUserInfo(): Promise { + if (!this.options.refreshToken) { + return null; + } + + const headers = this.getHeaders(); + const response = await this.http.get(`/v1/oauth2/userinfo`, { headers }); + + if (response.status !== 200) { + return null; + } + + const user = response.data as User; + + return user; + } +} diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..36fafdd --- /dev/null +++ b/src/constants.js @@ -0,0 +1,21 @@ +a// Copyright (c) Tribufu. All Rights Reserved. + +import packageJson from "../package.json"; + +/** + * The version of the Tribufu SDK. + * @type {string} + */ +export const TRIBUFU_VERSION = packageJson.version; + +/** + * The default Tribufu API URL. + * @type {string} + */ +export const TRIBUFU_API_URL = "https://api.tribufu.com"; + +/** + * The default Tribufu CDN URL. + * @type {string} + */ +export const TRIBUFU_CDN_URL = "https://cdn.tribufu.com"; diff --git a/src/http.ts b/src/http.ts new file mode 100644 index 0000000..577aedb --- /dev/null +++ b/src/http.ts @@ -0,0 +1,17 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +export type HeaderMap = { + [key: string]: string +}; + +export type CookieMap = { + [key: string]: string; +}; + +export interface ErrorResponse { + error: string; +}; + +export interface MessageResponse { + message: string; +}; diff --git a/src/index.ts b/src/index.ts index 6655681..7f1ad37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,536 +1,10 @@ // Copyright (c) Tribufu. All Rights Reserved. -import axios, { AxiosInstance } from "axios"; -import { OAuth2TokenRequest, OAuth2TokenResponse, OAuth2GrantType } from "./oauth2.ts"; - -export const TRIBUFU_VERSION = "0.0.0"; - -export interface User { - id: string; - uuid: string; - name: string; - displayName: string; - email?: string; - emailConfirmed: boolean; - type: string; - organizationId?: string; - privateFlags: string; - publicFlags: string; - official: boolean; - level: number; - experience: number; - publicBirthday: boolean; - birthday?: Date; - points: number; - parentalControl: boolean; - firstName?: string; - lastName?: string; - phone?: string; - phoneConfirmed: boolean; - location?: string; - timezone?: string; - language?: string; - currency?: string; - deleted: boolean; - lastIpAddress?: string; - registrationIpAddress?: string; - failedLogins: number; - twoFactorEnabled: boolean; - twoFactorKey?: string; - photoUrl: string; - bannerUrl: string; - status: string; - lastOnline?: Date; - biography?: string; - views: number; - created: Date; - updated?: Date; -} - -export interface SessionInfo { - user: User; - token: OAuth2TokenResponse; -}; - -export interface LoginRequest { - login: string; - password: string; - persistent: boolean | undefined; - state: string | null | undefined; -}; - -export type HeaderMap = { - [key: string]: string -}; - -export type CookieMap = { - [key: string]: string; -}; - -export enum RuntimeEnvironment { - Browser, - Node, - Other, -} - -/* -export type NextApiCookieContext = { req: NextApiRequest }; -export type NextPageCookieContext = Pick; -export type ExpressCookieContext = { req: Request; }; -export type CookieContext = NextApiCookieContext | NextPageCookieContext | ExpressCookieContext | null | undefined; -*/ - -export type ServerContext = any | null | undefined; - -export const TRIBUFU_API_URL = "https://api.tribufu.com"; - -export enum CredentialsType { - Anonymous, - ApiKey, - Client, -} - -export enum TokenType { - ApiKey, - Basic, - Bearer, -} - -export interface ApiKey { - kind: CredentialsType.ApiKey; - apiKey: string; -} - -export interface ClientCredentials { - kind: CredentialsType.Client; - clientId: string; - clientSecret: string; -} - -export type Credentials = ApiKey | ClientCredentials; - -export class TribufuApi { - private credentials: Credentials | null = null; - protected accessToken: string | null = null; - protected refreshToken: string | null = null; - private tokenType: TokenType | null = null; - protected readonly http: AxiosInstance; - - constructor(credentials: Credentials | null) { - if (credentials) { - switch (credentials.kind) { - case CredentialsType.ApiKey: - this.setApiKey(credentials.apiKey); - break; - case CredentialsType.Client: - this.setClient(credentials.clientId, credentials.clientSecret); - break; - } - } - - let http = axios.create({ - baseURL: TribufuApi.getBaseUrl(), - headers: TribufuApi.defaultHeaders(), - }); - - if (TribufuApi.debugEnabled()) { - http.interceptors.request.use((config) => { - console.log(`(TribufuApi) ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`); - return config; - }); - } - - this.http = http; - } - - public static default(): TribufuApi { - return new TribufuApi(null); - } - - private static debugEnabled(): boolean { - return process.env.NODE_ENV ? process.env.NODE_ENV === "development" : false; - } - - private static getBaseUrl(): string { - const baseUrl = process.env.TRIBUFU_API_URL || null; - return TribufuApi.debugEnabled() && baseUrl ? baseUrl : TRIBUFU_API_URL; - } - - private static userAgent(): string { - const targetTriple = "JavaScript"; - const userAgent = `Tribufu/${TRIBUFU_VERSION} (+https://api.tribufu.com; ${targetTriple}`; - return userAgent; - } - - private static defaultHeaders(): HeaderMap { - const headers = { - "User-Agent": TribufuApi.userAgent(), - "X-Tribufu-Language": "javascript", - "X-Tribufu-Version": TRIBUFU_VERSION, - }; - - return headers; - } - - private static detectRuntime(): RuntimeEnvironment { - if (typeof window !== "undefined") { - return RuntimeEnvironment.Browser; - } - - if (typeof process !== "undefined" && process?.versions?.node) { - return RuntimeEnvironment.Node; - } - - return RuntimeEnvironment.Other; - } - - public static isBrowser(): boolean { - return TribufuApi.detectRuntime() === RuntimeEnvironment.Browser; - } - - public static isNode(): boolean { - return TribufuApi.detectRuntime() === RuntimeEnvironment.Node; - } - - public static withApiKey(apiKey: string): TribufuApi { - return new TribufuApi({ - kind: CredentialsType.ApiKey, - apiKey, - }); - } - - public static withClient(clientId: string, clientSecret: string): TribufuApi { - return new TribufuApi({ - kind: CredentialsType.Client, - clientId, - clientSecret, - }); - } - - public static withApiKeyFromEnv(): TribufuApi { - const apiKey = process.env.TRIBUFU_API_URL_KEY; - - if (apiKey) { - return TribufuApi.withApiKey(apiKey); - } - - return TribufuApi.default(); - } - - public static withClientFromEnv(): TribufuApi { - const clientId = process.env.TRIBUFU_CLIENT_ID; - const clientSecret = process.env.TRIBUFU_CLIENT_SECRET; - - if (clientId && clientSecret) { - return TribufuApi.withClient(clientId, clientSecret); - } - - return TribufuApi.default(); - } - - /* - public static fromContext(context: ServerContext): TribufuApi { - return TribufuApi.fromCookies(nookies.get(context)); - } - */ - - public static fromCookies(cookies: CookieMap): TribufuApi { - let api = TribufuApi.withClientFromEnv(); - - const accessToken = cookies["access_token"]; - - if (accessToken) { - api?.setBearerToken(accessToken); - } - - return api; - } - - public setAnonymous() { - this.credentials = null; - } - - public setApiKey(apiKey: string) { - this.credentials = { kind: CredentialsType.ApiKey, apiKey }; - } - - public setClient(clientId: string, clientSecret: string) { - this.credentials = { kind: CredentialsType.Client, clientId, clientSecret }; - this.setBasicToken(Buffer.from(`${this.credentials.clientId}:${this.credentials.clientSecret}`, "binary").toString("base64")); - } - - public setBasicToken(basicToken: string) { - this.tokenType = TokenType.Basic; - this.accessToken = basicToken; - } - - public setBearerToken(accessToken: string, refreshToken?: string | null) { - this.tokenType = TokenType.Bearer; - this.accessToken = accessToken; - this.refreshToken = refreshToken || null; - } - - private getHeaders(): HeaderMap { - let headers: HeaderMap = {}; - - if (this.credentials?.kind === CredentialsType.ApiKey) { - headers["Authorization"] = `ApiKey ${this.credentials.apiKey}`; - return headers; - } - - switch (this.tokenType) { - case TokenType.Basic: - headers["Authorization"] = `Basic ${this.accessToken}`; - break; - case TokenType.Bearer: - headers["Authorization"] = `Bearer ${this.accessToken}`; - break; - } - - return headers; - } - - public async getTokenUsingAuthorizationCode(authorizationCode: string): Promise { - return await this.getToken("authorization_code", authorizationCode, null, null); - } - - public async getTokenUsingDeviceCode(deviceCode: string): Promise { - return await this.getToken("device_code", deviceCode, null, null); - } - - public async getTokenUsingRefreshToken(refreshToken: string): Promise { - return await this.getToken("refresh_token", refreshToken, null, null); - } - - public async getTokenUsingPassword(username: string, password: string): Promise { - return await this.getToken("password", password, null, username); - } - - public async getTokenUsingPasskey(username: string, passkey: string): Promise { - return await this.getToken("passkey", passkey, null, username); - } - - public async getTokenUsingClientCredentials(serverId?: string | null): Promise { - return await this.getToken("client_credentials", null, serverId ? "server_id" : null, serverId || null); - } - - private async getToken(grantType: OAuth2GrantType, grantValue: string | null, subjectKey: string | null, subjectValue: string | null): Promise { - try { - if (this.credentials?.kind !== CredentialsType.Client) { - return null; - } - - const credentials = this.credentials as ClientCredentials; - - const requestBody: OAuth2TokenRequest = { - grant_type: grantType, - code: grantType === "authorization_code" ? grantValue : null, - refresh_token: grantType === "refresh_token" ? grantValue : null, - username: grantType === "password" || grantType === "passkey" ? subjectValue : null, - password: grantType === "password" ? grantValue : null, - passkey: grantType === "passkey" ? grantValue : null, - client_id: credentials.clientId, - client_secret: credentials.clientSecret, - } - - const params = subjectKey && subjectValue ? `?${subjectKey}=${subjectValue}` : ""; - const url = `/v1/oauth2/token${params}`; - const headers = this.getHeaders(); - headers["Content-Type"] = "application/x-www-form-urlencoded"; - const response = await this.http.post(url, requestBody, { headers }); - - if (response.status !== 200) { - return null; - } - - const responseBody = response.data as OAuth2TokenResponse; - - return responseBody; - } catch (error) { - console.error(error) - return null; - } - } - - public async getUserInfo(): Promise { - try { - const headers = this.getHeaders(); - const response = await this.http.get(`/v1/oauth2/userinfo`, { headers }); - - if (response.status !== 200) { - return null; - } - - const user = response.data as User; - - return user; - } catch (error) { - console.error(error) - return null; - } - } - - public async getGames(page: number = 1): Promise { - try { - const headers = this.getHeaders(); - const response = await this.http.get(`/v1/packages?page=${page}`, { headers }); - - if (response.status !== 200) { - return []; - } - - const servers = response.data as any[]; - - return servers; - } catch (error) { - console.error(error) - return []; - } - } - - public async getGameyId(id: string): Promise { - try { - const headers = this.getHeaders(); - const response = await this.http.get(`/v1/packages/${id}`, { headers }); - - if (response.status !== 200) { - return []; - } - - const server = response.data as any; - - return server; - } catch (error) { - console.error(error) - return null; - } - } - - public async getServers(page: number = 1): Promise { - try { - const headers = this.getHeaders(); - const response = await this.http.get(`/v1/servers?page=${page}`, { headers }); - - if (response.status !== 200) { - return []; - } - - const servers = response.data as any[]; - - return servers; - } catch (error) { - console.error(error) - return []; - } - } - - public async getServer(idOrAddress: string): Promise { - if (isNaN(idOrAddress as any)) { - return await this.getServerByAddress(idOrAddress); - } - - return await this.getServerById(idOrAddress); - } - - public async getServerById(id: string): Promise { - try { - const headers = this.getHeaders(); - const response = await this.http.get(`/v1/servers/${id}`, { headers }); - - if (response.status !== 200) { - return []; - } - - const server = response.data as any; - - return server; - } catch (error) { - console.error(error) - return null; - } - } - - public async getServerByAddress(address: string): Promise { - try { - const headers = this.getHeaders(); - const response = await this.http.get(`/v1/servers/address/${address}`, { headers }); - - if (response.status !== 200) { - return []; - } - - const server = response.data as any; - - return server; - } catch (error) { - console.error(error) - return null; - } - } - - public async getUserById(id: string): Promise { - try { - const headers = this.getHeaders(); - const response = await this.http.get(`/v1/users/${id}`, { headers }); - - if (response.status !== 200) { - return null; - } - - const user = response.data as any; - - return user; - } catch (error) { - console.error(error) - return null; - } - } - - public async getUserByUuid(uuid: string): Promise { - return await this.getUserByKey("uuid", uuid); - } - - public async getUserByName(name: string): Promise { - return await this.getUserByKey("name", name); - } - - public async getUserByEmail(email: string): Promise { - return await this.getUserByKey("email", email); - } - - private async getUserByKey(key: string, value: string): Promise { - try { - const headers = this.getHeaders(); - const response = await this.http.get(`/v1/users/?${key}=${value}`, { headers }); - - if (response.status !== 200) { - return []; - } - - const users = response.data as any[]; - - return users; - } catch (error) { - console.error(error) - return []; - } - } - - public async getUserServers(userId: string): Promise { - try { - const headers = this.getHeaders(); - const response = await this.http.get(`/v1/users/${userId}/servers`, { headers }); - - if (response.status !== 200) { - return []; - } - - const servers = response.data as any[]; - - return servers; - } catch (error) { - console.error(error) - return []; - } - } -} +import { TRIBUFU_VERSION, TRIBUFU_API_URL, TRIBUFU_CDN_URL } from "./constants"; +import { TribufuApi } from "./api"; +import { TribufuBot } from "./bot"; +import { TribufuClient } from "./client"; +import { TribufuServer } from "./server"; + +export { TRIBUFU_VERSION, TRIBUFU_API_URL, TRIBUFU_CDN_URL }; +export { TribufuApi, TribufuBot, TribufuClient, TribufuServer }; diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..ed3de68 --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,26 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +export type AccountType = "user" | "bot" | "org"; + +export interface User { + id: number, + uuid: string, + name: string, + display_name: string, + kind: AccountType, + public_flags: number, + verified: boolean, + level: number, + experience: number, + public_birthday: boolean, + birthday?: string | null, + points: number, + location?: string | null, + photo_url?: string | null, + banner_url?: string | null, + last_online?: string | null, + biography?: string | null, + view_count: number, + created: string, + updated?: string | null, +} diff --git a/src/node.ts b/src/node.ts new file mode 100644 index 0000000..17b6150 --- /dev/null +++ b/src/node.ts @@ -0,0 +1,7 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +export enum JavaScriptRuntime { + Browser, + Node, + Other, +} diff --git a/src/oauth2.ts b/src/oauth2.ts index 61c5fa3..1bb1df5 100644 --- a/src/oauth2.ts +++ b/src/oauth2.ts @@ -32,6 +32,11 @@ export interface OAuth2TokenRequest { redirect_uri?: string | null; }; +export interface OAuth2RevokeRequest { + token: string; + token_type_hint: OAuth2TokenHintType; +}; + export interface OAuth2TokenResponse { token_type: OAuth2TokenType; access_token: string; @@ -41,7 +46,7 @@ export interface OAuth2TokenResponse { expires_in: number; }; -export interface OAuth2RevokeRequest { +export interface OAuth2IntrospectionRequest { token: string; token_type_hint: OAuth2TokenHintType; }; diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..d39ec99 --- /dev/null +++ b/src/options.ts @@ -0,0 +1,8 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +export interface TribufuApiOptions { + apiKey?: string | null; + accessToken?: string | null; + refreshToken?: string | null; + expiresIn?: number | null; +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..2c3973b --- /dev/null +++ b/src/server.ts @@ -0,0 +1,57 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +import { TribufuApi } from "./api"; +import { TribufuClient } from "./client"; + +/** + * **Tribufu Server** + * + * To authenticate a server you need to use the server id, client id and client secret obtained from your server subscription. + * + * - A server is a special type of client application. + * - A server is how game servers interact with the Tribufu API. + * - A server give you read and write access to the Tribufu API. + */ +export class TribufuServer extends TribufuClient { + private readonly serverId: string; + + constructor(serverId: string, clientId: string, clientSecret: string) { + super(clientId, clientSecret); + this.serverId = serverId; + } + + /** + * Try to create a TribufuServer from environment variables. + * + * - This will only work if the environment variables are set. + * + * @param prefix A prefix for the environment variables. + * @returns TribufuServer | null + * @example + * ```ts + * // process.env.TRIBUFU_SERVER_ID + * // process.env.TRIBUFU_CLIENT_ID + * // process.env.TRIBUFU_CLIENT_SECRET + * const server = TribufuServer.fromEnv("TRIBUFU_"); + * ``` + */ + public static override fromEnv(prefix: string = ""): TribufuServer | null { + const serverId = process.env[`${prefix}SERVER_ID`]; + const clientId = process.env[`${prefix}CLIENT_ID`]; + const clientSecret = process.env[`${prefix}CLIENT_SECRET`]; + + if (serverId && clientId && clientSecret) { + return TribufuApi.withServer(serverId, clientId, clientSecret); + } + + return null; + } + + /** + * Get a list of connected users. + * @returns + */ + public async getConnectedUsers(): Promise { + return []; + } +} diff --git a/tsconfig.json b/tsconfig.json index 1d7bba4..bdc4015 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,8 @@ "moduleResolution": "Node", "esModuleInterop": true, "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "rootDir": "src", "outDir": "build", "lib": [ "ESNext",