diff --git a/.vscode/settings.json b/.vscode/settings.json index 2368d2d786..6c64443301 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,6 +57,7 @@ "editor.insertSpaces": true, "editor.tabSize": 4, "editor.detectIndentation": false, + "javascript.preferences.importModuleSpecifier": "relative", "javascript.preferences.quoteStyle": "single", "typescript.preferences.quoteStyle": "single", "workbench.colorCustomizations": {}, @@ -64,5 +65,6 @@ "jest.autoEnable": false, "typescript.tsdk": "node_modules\\typescript\\lib", "typescript.tsserver.maxTsServerMemory": 8192, - "jest.autoRun": "off" + "jest.autoRun": "off", + "cSpell.words": ["KASP"] } diff --git a/docker/conf/redis.conf b/docker/conf/redis.conf new file mode 100644 index 0000000000..89dfa14695 --- /dev/null +++ b/docker/conf/redis.conf @@ -0,0 +1,2 @@ +bind 0.0.0.0 :: +protected-mode no \ No newline at end of file diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 25de3b8c3e..c67f48708e 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -37,7 +37,9 @@ services: - '6379:6379' networks: - enable_ipv6 - command: ['redis-server', '--bind', '0.0.0.0', '::'] + volumes: + - ./conf/redis.conf:/etc/redis.conf + command: ['redis-server', '/etc/redis.conf'] livekit: image: livekit/livekit-server:v1.8.0 restart: always diff --git a/jakefile.js b/jakefile.js index 522acda179..9a0c9af6a9 100644 --- a/jakefile.js +++ b/jakefile.js @@ -78,7 +78,7 @@ task('generate-stub-projects', [], async function () { const { existsSync, writeFileSync, mkdirSync } = await import('fs'); const projects = [ - path.resolve(__dirname, 'xpexchange', 'xp-api', 'tsconfig.json'), + path.resolve(__dirname, 'xpexchange', 'tsconfig.json'), path.resolve( __dirname, 'extensions', diff --git a/src/aux-records/MemoryStore.ts b/src/aux-records/MemoryStore.ts index 0b3edc1a86..0a36911d6a 100644 --- a/src/aux-records/MemoryStore.ts +++ b/src/aux-records/MemoryStore.ts @@ -1,5 +1,5 @@ import { cloneDeep, orderBy, sortBy } from 'lodash'; -import { RegexRule } from './Utils'; +import { cloneDeepNull, RegexRule } from './Utils'; import { AddressType, AuthInvoice, @@ -149,6 +149,15 @@ import { } from './SystemNotificationMessenger'; import { ModerationConfiguration } from './ModerationConfiguration'; import { uniq } from 'lodash'; +import { + XpAccount, + XpAccountEntry, + XpContract, + XpInvoice, + XpStore, + XpUser, +} from './XpStore'; +import { SuccessResult } from './TypeUtils'; export interface MemoryConfiguration { subscriptions: SubscriptionConfiguration; @@ -168,7 +177,8 @@ export class MemoryStore ConfigurationStore, InstRecordsStore, ModerationStore, - SystemNotificationMessenger + SystemNotificationMessenger, + XpStore { private _users: AuthUser[] = []; private _userAuthenticators: AuthUserAuthenticator[] = []; @@ -213,6 +223,13 @@ export class MemoryStore private _studioLoomConfigs: Map = new Map(); private _studioHumeConfigs: Map = new Map(); + private _xpUsers: Map = new Map(); + private _xpAccounts: Map = new Map(); + private _xpContracts: Map = new Map(); + private _xpInvoices: Map = new Map(); + private _xpAccountEntries: Map = + new Map(); + // TODO: Support global permissions // private _globalPermissionAssignments: GlobalPermissionAssignment[] = []; @@ -351,6 +368,69 @@ export class MemoryStore this.roleAssignments = {}; } + async saveXpAccount( + associationId: XpUser['id'] | XpContract['id'], + account: XpAccount + ): Promise { + this._xpAccounts.set(associationId, account); + return { + success: true, + }; + } + + async saveXpUserWithAccount(user: XpUser, account: XpAccount) { + this._xpUsers.set(user.id, user); + this._xpAccounts.set(account.id, account); + } + + async saveXpUser(id: XpUser['id'], user: XpUser): Promise { + this._xpUsers.set(id, user); + return { + success: true, + }; + } + + async saveXpContract( + contract: XpContract, + account: XpAccount | null + ): Promise { + this._xpContracts.set(contract.id, contract); + if (account !== null) { + this._xpAccounts.set(account.id, account); + } + } + + async getXpUserByAuthId(id: AuthUser['id']): Promise { + const user = Array.from(this._xpUsers.values()).find( + (u: XpUser) => u.userId === id + ); + return cloneDeepNull(user ?? undefined); + } + + async getXpUserById(id: XpUser['id']): Promise { + return cloneDeep(this._xpUsers.get(id) ?? undefined); + } + + async getXpAccount( + associationId: XpUser['id'] | XpContract['id'] + ): Promise { + return cloneDeepNull(this._xpAccounts.get(associationId) ?? undefined); + } + + async getXpAccountEntry( + entryId: XpAccountEntry['id'] + ): Promise { + return cloneDeepNull(this._xpAccountEntries.get(entryId) ?? undefined); + } + + async getXpContract(contractId: XpContract['id']): Promise { + return cloneDeepNull(this._xpContracts.get(contractId) ?? undefined); + } + + async getXpInvoice(invoiceId: XpInvoice['id']): Promise { + return cloneDeepNull(this._xpInvoices.get(invoiceId) ?? undefined); + } + init?(): Promise; /** diff --git a/src/aux-records/RecordsServer.ts b/src/aux-records/RecordsServer.ts index e82fd68d3d..93b69fce7f 100644 --- a/src/aux-records/RecordsServer.ts +++ b/src/aux-records/RecordsServer.ts @@ -129,6 +129,7 @@ import { PUSH_NOTIFICATION_PAYLOAD, PUSH_SUBSCRIPTION_SCHEMA, } from './notifications'; +import { XpController } from './XpController'; declare const GIT_TAG: string; declare const GIT_HASH: string; @@ -365,8 +366,54 @@ export interface RecordsServerOptions { * If null, then notifications are not supported. */ notificationsController?: NotificationRecordsController | null; + + /** + * The controller that should be used for handling XP requests. + * If null, then XP is not supported. + */ + xpController?: XpController | null; // TODO: Determine whether or not this should be optional } +/** + * A schema that represents a request to get an XP user by one of it's IDs. + */ +const GetXpUserById = z + .object({ + userId: z.string().optional().nullable(), + xpId: z.string().optional().nullable(), + }) + .refine( + (contractedUser) => { + if (!contractedUser) return true; + if ( + contractedUser.userId ?? + (undefined === undefined && contractedUser.xpId) ?? + undefined === undefined + ) + return false; + return true; + }, + { + message: 'One of properties "userId", "xpId" are required', + path: ['userId', 'xpId'], + } + ) + .refine( + (contractedUser) => { + if (!contractedUser) return true; + if ( + typeof contractedUser.userId === 'string' && + typeof contractedUser.xpId === 'string' + ) + return false; + return true; + }, + { + message: 'Properties userId and xpId are mutually exclusive.', + path: ['userId', 'xpId'], + } + ); + /** * Defines a class that represents a generic HTTP server suitable for Records HTTP Requests. */ @@ -385,6 +432,7 @@ export class RecordsServer { private _loomController: LoomController | null; private _webhooksController: WebhookRecordsController | null; private _notificationsController: NotificationRecordsController | null; + private _xpController: XpController | null; /** * The set of origins that are allowed for API requests. @@ -452,6 +500,7 @@ export class RecordsServer { loomController, webhooksController, notificationsController, + xpController, }: RecordsServerOptions) { this._allowedAccountOrigins = allowedAccountOrigins; this._allowedApiOrigins = allowedApiOrigins; @@ -473,6 +522,7 @@ export class RecordsServer { this._loomController = loomController; this._webhooksController = webhooksController; this._notificationsController = notificationsController; + this._xpController = xpController; this._tracer = trace.getTracer( 'RecordsServer', typeof GIT_TAG === 'undefined' ? undefined : GIT_TAG @@ -2090,6 +2140,178 @@ export class RecordsServer { return result; }), + getXpUserMeta: procedure() + .origins('api') + .http('GET', '/api/v2/xp/user') + .inputs(GetXpUserById) + .handler(async (input, context) => { + const authUser = await this._validateSessionKey( + context.sessionKey + ); + if (!authUser.success) { + return authUser; + } + if (!input.userId && !input.xpId && !authUser.userId) { + return { + success: false, + errorCode: 'unacceptable_request', + errorMessage: + 'One of properties userId or xpId must be provided.', + }; + } + if (input.userId && input.xpId) { + return { + success: false, + errorCode: 'unacceptable_request', + errorMessage: + 'You cannot provide both userId and xpId.\nProperties are mutually exclusive.', + }; + } + //* An empty string for any of the query types will be treated as the current logged in user + const user = await this._xpController.getXpUser( + input.xpId + ? { xpId: input.xpId } + : input.userId + ? { userId: input.userId } + : { userId: authUser.userId } + ); + return user; + }), + + createXpContract: procedure() + .origins('api') + .http('POST', '/api/v2/xp/contract') + .inputs( + z.object({ + contract: z + .object({ + description: z.string().optional(), + accountCurrency: z.string().optional(), + gigRate: z.number().int().positive(), + gigs: z.number().int().positive(), + status: z.union([ + z.literal('open'), + z.literal('draft'), + ]), + contractedUserId: + GetXpUserById.optional().nullable(), + }) + .refine( + (contract) => { + if (contract.status === 'open') { + //* Open contracts must have a contracted user + return ( + contract.contractedUserId ?? + undefined !== undefined + ); + } else if (contract.status === 'draft') { + /** + * * Draft contracts from the database's perspective should not be expecting a contracted user yet. + * * This is because the contracted user is only set when the contract is opened. + */ + return ( + contract.contractedUserId ?? + undefined === undefined + ); + } + }, + { + message: + 'Contract must contain contractedUserId (only when status is "open").', + path: ['contractedUserId'], + } + ) + .refine( + (contract) => { + if (contract.status === 'open') { + return ( + contract.accountCurrency ?? + undefined !== undefined + ); + } else if (contract.status === 'draft') { + return ( + contract.accountCurrency ?? + undefined === undefined + ); + } + }, + { + message: + 'Contract must contain accountCurrency (only when status is "open").', + path: ['accountCurrency'], + } + ), + }) + ) + .handler(async ({ contract }, context) => { + const authUser = await this._validateSessionKey( + context.sessionKey + ); + if (!authUser.success) { + return authUser; + } + // const result = await this._xpController.createContract({ + // description: contract.description ?? null, + // accountCurrency: contract.accountCurrency, + // rate: contract.gigRate, + // offeredWorth: contract.gigs + // ? (contract.gigRate ?? 0) * contract.gigs + // : 0, + // status: contract.status, + // issuerUserId: { userId: authUser.userId }, + // holdingUserId: contract.contractedUserId, + // creationRequestReceivedAt: Date.now(), + // }); + // return result; + return { + success: true, + message: 'Not implemented', + }; + }), + + updateXpContract: procedure() + .origins('api') + .http('PUT', '/api/v2/xp/contract') + .inputs( + z.object({ + contractId: z.string().min(1), + newStatus: z.union([ + z.literal('open'), + z.literal('draft'), + z.literal('closed'), + ]), + //* Note this is the change of the description which can only be done when the contract is in draft status + newDescription: z.string().optional(), + receivingUserId: GetXpUserById, + }) + ) + .handler( + async ( + { contractId, newStatus, newDescription }, + context + ) => { + const authUser = await this._validateSessionKey( + context.sessionKey + ); + if (!authUser.success) { + return authUser; + } + // const result = await this._xpController.updateContract({ + // contractId, + // newStatus, + // newDescription, + // userId: authUser.userId, + // }); + // throw new Error('Not implemented'); + //return result; + // TODO: Implement. + return { + success: true, + message: 'Not implemented', + }; + } + ), + listRecords: procedure() .origins('api') .http('GET', '/api/v2/records/list') diff --git a/src/aux-records/SloydInterface.ts b/src/aux-records/SloydInterface.ts index b3ad711bb0..47df29d0b3 100644 --- a/src/aux-records/SloydInterface.ts +++ b/src/aux-records/SloydInterface.ts @@ -8,7 +8,7 @@ import { SloydModelMimeTypes, } from './AISloydInterface'; import axios from 'axios'; -import { handleAxiosErrors } from 'Utils'; +import { handleAxiosErrors } from './Utils'; export interface SloydOptions { clientId: string; diff --git a/src/aux-records/TestUtils.ts b/src/aux-records/TestUtils.ts index 58d9fbca9e..0e137501ad 100644 --- a/src/aux-records/TestUtils.ts +++ b/src/aux-records/TestUtils.ts @@ -14,6 +14,7 @@ import { buildSubscriptionConfig, SubscriptionConfigBuilder, } from './SubscriptionConfigBuilder'; +import { XpController } from './XpController'; export type TestServices = ReturnType; @@ -81,6 +82,11 @@ export function createTestControllers( messenger: store, }); const policies = new PolicyController(auth, records, store); + const xpController = new XpController({ + authController: auth, + authStore: store, + xpStore: store, + }); return { store, @@ -92,6 +98,7 @@ export function createTestControllers( policyStore: store, policies, configStore: store, + xpController, }; } @@ -141,6 +148,18 @@ export async function createTestUser( }; } +export async function createTestXpUser( + xpController: XpController, + ...createTestUserParams: Parameters +) { + const authUser = await createTestUser(...createTestUserParams); + const xpUser = await xpController.getXpUser({ userId: authUser.userId }); + if (!xpUser.success) { + throw new Error('Unable to create xp user!'); + } + return xpUser.user; +} + export async function createTestRecordKey( { records }: Pick, userId: string, diff --git a/src/aux-records/TypeUtils.ts b/src/aux-records/TypeUtils.ts new file mode 100644 index 0000000000..f6b036d175 --- /dev/null +++ b/src/aux-records/TypeUtils.ts @@ -0,0 +1,461 @@ +/** + * A map of ISO4217 alphabetic currency codes to their numeric codes + * and minor units + * * Last updated: 2024-08-30 (YYYY-MM-DD) + */ +export type ISO4217_Map = { + AED: { n: 784; mU: 2 }; + AFN: { n: 971; mU: 2 }; + ALL: { n: 8; mU: 2 }; + AMD: { n: 51; mU: 2 }; + ANG: { n: 532; mU: 2 }; + AOA: { n: 973; mU: 2 }; + ARS: { n: 32; mU: 2 }; + AUD: { n: 36; mU: 2 }; + AWG: { n: 533; mU: 2 }; + AZN: { n: 944; mU: 2 }; + BAM: { n: 977; mU: 2 }; + BBD: { n: 52; mU: 2 }; + BDT: { n: 50; mU: 2 }; + BGN: { n: 975; mU: 2 }; + BHD: { n: 48; mU: 3 }; + BIF: { n: 108; mU: 0 }; + BMD: { n: 60; mU: 2 }; + BND: { n: 96; mU: 2 }; + BOB: { n: 68; mU: 2 }; + BOV: { n: 984; mU: 2 }; + BRL: { n: 986; mU: 2 }; + BSD: { n: 44; mU: 2 }; + BTN: { n: 64; mU: 2 }; + BWP: { n: 72; mU: 2 }; + BYN: { n: 933; mU: 2 }; + BZD: { n: 84; mU: 2 }; + CAD: { n: 124; mU: 2 }; + CDF: { n: 976; mU: 2 }; + CHE: { n: 947; mU: 2 }; + CHF: { n: 756; mU: 2 }; + CHW: { n: 948; mU: 2 }; + CLF: { n: 990; mU: 4 }; + CLP: { n: 152; mU: 0 }; + CNY: { n: 156; mU: 2 }; + COP: { n: 170; mU: 2 }; + COU: { n: 970; mU: 2 }; + CRC: { n: 188; mU: 2 }; + CUC: { n: 931; mU: 2 }; + CUP: { n: 192; mU: 2 }; + CVE: { n: 132; mU: 2 }; + CZK: { n: 203; mU: 2 }; + DJF: { n: 262; mU: 0 }; + DKK: { n: 208; mU: 2 }; + DOP: { n: 214; mU: 2 }; + DZD: { n: 12; mU: 2 }; + EGP: { n: 818; mU: 2 }; + ERN: { n: 232; mU: 2 }; + ETB: { n: 230; mU: 2 }; + EUR: { n: 978; mU: 2 }; + FJD: { n: 242; mU: 2 }; + FKP: { n: 238; mU: 2 }; + GBP: { n: 826; mU: 2 }; + GEL: { n: 981; mU: 2 }; + GHS: { n: 936; mU: 2 }; + GIP: { n: 292; mU: 2 }; + GMD: { n: 270; mU: 2 }; + GNF: { n: 324; mU: 0 }; + GTQ: { n: 320; mU: 2 }; + GYD: { n: 328; mU: 2 }; + HKD: { n: 344; mU: 2 }; + HNL: { n: 340; mU: 2 }; + HTG: { n: 332; mU: 2 }; + HUF: { n: 348; mU: 2 }; + IDR: { n: 360; mU: 2 }; + ILS: { n: 376; mU: 2 }; + INR: { n: 356; mU: 2 }; + IQD: { n: 368; mU: 3 }; + IRR: { n: 364; mU: 2 }; + ISK: { n: 352; mU: 0 }; + JMD: { n: 388; mU: 2 }; + JOD: { n: 400; mU: 3 }; + JPY: { n: 392; mU: 0 }; + KES: { n: 404; mU: 2 }; + KGS: { n: 417; mU: 2 }; + KHR: { n: 116; mU: 2 }; + KMF: { n: 174; mU: 0 }; + KPW: { n: 408; mU: 2 }; + KRW: { n: 410; mU: 0 }; + KWD: { n: 414; mU: 3 }; + KYD: { n: 136; mU: 2 }; + KZT: { n: 398; mU: 2 }; + LAK: { n: 418; mU: 2 }; + LBP: { n: 422; mU: 2 }; + LKR: { n: 144; mU: 2 }; + LRD: { n: 430; mU: 2 }; + LSL: { n: 426; mU: 2 }; + LYD: { n: 434; mU: 3 }; + MAD: { n: 504; mU: 2 }; + MDL: { n: 498; mU: 2 }; + MGA: { n: 969; mU: 2 }; + MKD: { n: 807; mU: 2 }; + MMK: { n: 104; mU: 2 }; + MNT: { n: 496; mU: 2 }; + MOP: { n: 446; mU: 2 }; + MRU: { n: 929; mU: 2 }; + MUR: { n: 480; mU: 2 }; + MVR: { n: 462; mU: 2 }; + MWK: { n: 454; mU: 2 }; + MXN: { n: 484; mU: 2 }; + MXV: { n: 979; mU: 2 }; + MYR: { n: 458; mU: 2 }; + MZN: { n: 943; mU: 2 }; + NAD: { n: 516; mU: 2 }; + NGN: { n: 566; mU: 2 }; + NIO: { n: 558; mU: 2 }; + NOK: { n: 578; mU: 2 }; + NPR: { n: 524; mU: 2 }; + NZD: { n: 554; mU: 2 }; + OMR: { n: 512; mU: 3 }; + PAB: { n: 590; mU: 2 }; + PEN: { n: 604; mU: 2 }; + PGK: { n: 598; mU: 2 }; + PHP: { n: 608; mU: 2 }; + PKR: { n: 586; mU: 2 }; + PLN: { n: 985; mU: 2 }; + PYG: { n: 600; mU: 0 }; + QAR: { n: 634; mU: 2 }; + RON: { n: 946; mU: 2 }; + RSD: { n: 941; mU: 2 }; + RUB: { n: 643; mU: 2 }; + RWF: { n: 646; mU: 0 }; + SAR: { n: 682; mU: 2 }; + SBD: { n: 90; mU: 2 }; + SCR: { n: 690; mU: 2 }; + SDG: { n: 938; mU: 2 }; + SEK: { n: 752; mU: 2 }; + SGD: { n: 702; mU: 2 }; + SHP: { n: 654; mU: 2 }; + SLE: { n: 925; mU: 2 }; + SOS: { n: 706; mU: 2 }; + SRD: { n: 968; mU: 2 }; + SSP: { n: 728; mU: 2 }; + STN: { n: 930; mU: 2 }; + SVC: { n: 222; mU: 2 }; + SYP: { n: 760; mU: 2 }; + SZL: { n: 748; mU: 2 }; + THB: { n: 764; mU: 2 }; + TJS: { n: 972; mU: 2 }; + TMT: { n: 934; mU: 2 }; + TND: { n: 788; mU: 3 }; + TOP: { n: 776; mU: 2 }; + TRY: { n: 949; mU: 2 }; + TTD: { n: 780; mU: 2 }; + TWD: { n: 901; mU: 2 }; + TZS: { n: 834; mU: 2 }; + UAH: { n: 980; mU: 2 }; + UGX: { n: 800; mU: 0 }; + USD: { n: 840; mU: 2 }; + USN: { n: 997; mU: 2 }; + UYI: { n: 940; mU: 0 }; + UYU: { n: 858; mU: 2 }; + UYW: { n: 927; mU: 4 }; + UZS: { n: 860; mU: 2 }; + VED: { n: 926; mU: 2 }; + VES: { n: 928; mU: 2 }; + VND: { n: 704; mU: 0 }; + VUV: { n: 548; mU: 0 }; + WST: { n: 882; mU: 2 }; + XAF: { n: 950; mU: 0 }; + XAG: { n: 961; mU: null }; + XAU: { n: 959; mU: null }; + XBA: { n: 955; mU: null }; + XBB: { n: 956; mU: null }; + XBC: { n: 957; mU: null }; + XBD: { n: 958; mU: null }; + XCD: { n: 951; mU: 2 }; + XDR: { n: 960; mU: null }; + XOF: { n: 952; mU: 0 }; + XPD: { n: 964; mU: null }; + XPF: { n: 953; mU: 0 }; + XPT: { n: 962; mU: null }; + XSU: { n: 994; mU: null }; + XTS: { n: 963; mU: null }; + XUA: { n: 965; mU: null }; + XXX: { n: 999; mU: null }; + YER: { n: 886; mU: 2 }; + ZAR: { n: 710; mU: 2 }; + ZMW: { n: 967; mU: 2 }; + ZWG: { n: 924; mU: 2 }; + ZWL: { n: 932; mU: 2 }; +}; + +/** + * A Generic utility type that parses a type's keys and properties from + * sub-objects into an array of arrays each containing each key and + * its requested properties + * @template U The root type + * @template A An array of properties to extract from each key (U[T]) in U + * @template T Used to force union effects on all keys, please do not override + * ? What is KASP? An acronym for Keys And Specified Properties :) + * @example + * type X = { + * a: { x: 'String literal'; y: number, z: boolean }; + * b: { x: string; y: number, z: boolean }; + * }; + * type Y = ArrayOfKASP; + * // [['a','String literal','number'],['b','string','number']] + */ +export type ArrayOfKASP< + U, + A extends Array, + T extends keyof U = keyof U +> = Array< + T extends keyof U + ? [ + T, + ...{ + [K in keyof A]: A[K] extends keyof U[T] ? U[T][A[K]] : never; + } + ] + : never +>; + +/** + * A Generic utility type which returns the opposite of the extends operator (!extends) + * @template T The type to check if it does not extend U + * @template U The type to check if T does not extend it + */ +export type NotExtends = T extends U ? false : true; + +/** + * A Generic utility type which return the specified properties of a type + * @template T The type to extract properties from + * @template Keys The keys to extract from T + */ +export type PropertiesOf = { + [Key in K]: T[K]; +}[K]; + +/** + * A Generic utility type which converts a union of keys + * to another union of keys which conform to the paradigm `${P}${Key}${S}` + * @template T The Union of keys to apply the prefix/suffix to + * @template P The prefix to apply to each key + * @template S The suffix to apply to each key + * @template A A boolean flag to determine whether to apply the prefix/suffix or remove it + * @template C A boolean flag to determine whether to capitalize / uncapitalize the key + * @example + * type UnionToAlias = 'keyX' | 'keyY' | 'keyZ'; + * type Aliased = AliasUnion; // "omitKeyX" | "omitKeyY" | "omitKeyZ" + * type De_Aliased = AliasUnion; // "keyX" | "keyY" | "keyZ" + */ +export type AliasUnion< + T extends string, + P extends string = '_', + S extends string = '', + A extends boolean = true, + C extends boolean = true +> = A extends true + ? T extends string + ? `${P}${Capitalize}${S}` + : never + : T extends string & `${P}${infer Key}${S}` + ? C extends true + ? Uncapitalize + : Key + : never; + +/** + * A Generic utility type which maps original keys to aliased keys + * @template T The union of strings to map to aliases + * @template P The prefix to apply to each key + * @template S The suffix to apply to each key + * @template R A boolean flag to determine whether to reverse the mapping (true = original to alias) + * @example + * type UnionToAlias = 'keyX' | 'keyY' | 'keyZ'; + * type AliasM = AliasMap; // { "omitKeyX": "keyX", "omitKeyY": "keyY", "omitKeyZ": "keyZ" } + * type ReverseAliasM = AliasMap; // { "keyX": "omitKeyX", "keyY": "omitKeyY", "keyZ": "omitKeyZ" } + */ +export type AliasMap< + T extends string, + P extends string = '_', + S extends string = '', + R extends boolean = false +> = R extends true + ? { [K in T]: AliasUnion } + : { [A in AliasUnion]: AliasUnion }; + +/** + * A Generic utility type which allows a value to be either itself or null + * @template T The type to make nullable + */ +export type Nullable = T | null; + +export type NotNullOrOptional = { + [K in keyof T]-?: Exclude; +}; + +/** + * A Generic utility type which flattens arrays into unions containing the array's elements + * @template T The type to flatten (can contain non-array types which will be included in the returned union) + * @example + * type X = FlattenUnion<[1,2,3]>; // 1 | 2 | 3 + * type Y = FlattenUnion<[1,2,3] | 'a' | 'b'>; // 1 | 2 | 3 | 'a' | 'b' + */ +export type FlattenUnion = T extends Array ? U : T; + +/** + * A Generic utility type which forms a union of the values of a given type + * * Is not inherently recursive, will only extract the first level of keys + * @template T The type to extract values from + * @example + * type X = { a: 1; b: 2; c: 3 }; + * type Y = UnionOfTValues; // 1 | 2 | 3 + */ +export type UnionOfTValues = K extends keyof T ? T[K] : never; + +/** Provides an extensible interface for commonly used Date markers */ +export interface GenericTimeKeys { + /** The date at which the entity was created */ + createdAtMs: DateMS; + /** The date at which the entity was last updated */ + updatedAtMs: DateMS; +} + +/** + * A Generic utility type which extracts id's as a union of their keys from a given type T + * @template T The type to extract id's from + * @template S The suffix to search for in the keys of T, defaults to 'Id' + * @template X The keys of T to search for, defaults to all keys of T + */ +export type IdStrIn< + T, + S extends string = 'Id', + X extends keyof T = keyof T +> = X extends `${infer _}${S}` | 'id' ? X : never; + +/** + * A Generic utility type which extracts id's from a given type T + * @template T The type to extract id's from + */ +export type IdIn = Pick>; + +/** A type alias for a number, that should only represent time in milliseconds */ +export type DateMS = number; +/** A type alias for a number, that should only represent a quantity of gigs (6 minute intervals of work) */ +export type GigQty = number; +/** A type alias for a number, that should only represent a quantity of a currency in its smallest fractional unit (E.G. cents in USD) */ +export type CurrencySFU = number; + +/** + * A Generic utility type which compares a type to "never" providing a boolean representation as to whether or not + * the aforementioned type is "never" + */ +export type IsNever = [T] extends [never] ? true : false; + +/** + * A Generic utility type which extracts the property on type T at K when K is a key of T; + * but does not constrain K to be a key of T + * @template T The type to extract the property from + * @template K The key to extract the property at + * @example + * type X = { a: 1; b: 2; c: 3 }; + * type Y = KNever; // 1 + * type Z = KNever; // never + */ +export type KNever = K extends keyof T ? T[K] : never; + +/** + * A Generic utility type which extracts from S keys which are present in P + * @template P The type which contains the keys to extract from S if they are present + * @template S The type to extract keys from + * @example + * type P = 'a' | 'b' | 'c'; + * type S = 'b' | 'c' | 'd'; + * type X = ExtractKeys; // 'b' | 'c' + */ +export type OfPUnion = S extends P ? S : never; + +/** + * A Generic utility type which represents either T or a Promise of T + * @template T The type to represent as a Promise or an immediate value of + */ +export type PromiseOrValue = T | Promise; + +/** + * A niched utility type which performs the core advertised functionality of ProviderIfInHost, and + * converts type E to a type which provides values which are members of Host H + * @template E The (entity) type to convert + * @template H The (host) type which contains types that should be converted to provider functions + */ +export type EntityProvider = { + [K in P]: E[K] extends UnionOfTValues + ? (...args: any) => PromiseOrValue> + : E[K] extends UnionOfTValues[] + ? ProviderIfInHost + : E[K]; +}; + +/** + * A Generic utility type which converts a type to a type which provides values + * which are members of Host H (provider functions). + * * Useful for preventing direct recursive access to members which are described by the host + * @template T The type to convert + * @template H The host type which contains types that should be converted to provider functions + */ +export type ProviderIfInHost = T extends Array + ? (...args: any) => PromiseOrValue<{ + [K in keyof T]: EntityProvider; + }> + : EntityProvider; + +/** + * An abstract type which represents a "model" host, a niched type, grouping types which may reference each other + * @template T A type which contains types that may be self-referential (from the host context) + * @example + * type X = ModelHost<{ + * a: { x: 'String literal'; y: number, z: boolean }; // Target within host X + * b: X['a'][]; // Self-referential property of host X + * }>; + * + */ +export type ModelHost> = T; + +/** + * A Generic utility type which extracts the keys of a type T which are present in a type H + */ +export type StoreOfModel< + M extends ModelHost, + A extends keyof any +> = A extends keyof any ? keyof M : never; +// TODO: Complete the implementation of the StoreOfModel type, should return a record of keys in M which are modified by each union A member + +/** + * A Generic utility type which allows for extensible Action results to be defined with a success state + * @template S The success state of the action + * @template T The type of the result to be included in the action result + */ +export type ActionResult_T = { + /** The result success state of the action */ + success: S; +} & T; + +/** + * A Generic utility type which represents an action result with a success state and a result + * @template S The success state of the action + * @template T The type of the result to be included in the action result + */ +export type SuccessResult< + S extends boolean = true | false, + T extends object = S extends true + ? {} + : { + /** The error code */ + errorCode: string; + /** The error message */ + errorMessage?: string; + } +> = ActionResult_T; + +/** + * A Generic Promise utility type which extracts the type of a promise's resolved value + * @template T The type of the promise to extract the resolved value from + */ +export type PromiseT = T extends Promise ? U : T; diff --git a/src/aux-records/Utils.spec.ts b/src/aux-records/Utils.spec.ts index 547496a0d7..0eed6616b2 100644 --- a/src/aux-records/Utils.spec.ts +++ b/src/aux-records/Utils.spec.ts @@ -16,6 +16,7 @@ import { byteLengthOfString, getRootMarker, getPathMarker, + getISO4217CurrencyCode, } from './Utils'; describe('signRequest()', () => { @@ -424,6 +425,17 @@ describe('encodeHexUtf8()', () => { ); }); +describe('getISO4217CurrencyCode()', () => { + it('should get an ISO4217 currency code and meta', () => { + // Expected currency code meta + const expected: ['USD', 840, 2] = ['USD', 840, 2]; + // Test with Alphabetical currency code + expect(getISO4217CurrencyCode(expected[0])).toEqual(expected); + // Test with Numeric currency code + expect(getISO4217CurrencyCode(expected[1])).toEqual(expected); + }); +}); + describe('parseInstancesList()', () => { it('should return undefined if given an empty string', () => { expect(parseInstancesList('')).toEqual(undefined); diff --git a/src/aux-records/Utils.ts b/src/aux-records/Utils.ts index b8dc9827e7..ea7ded0da7 100644 --- a/src/aux-records/Utils.ts +++ b/src/aux-records/Utils.ts @@ -2,6 +2,7 @@ import _, { omitBy, padStart, sortBy } from 'lodash'; import { sha256, hmac } from 'hash.js'; import { PUBLIC_READ_MARKER } from '@casual-simulation/aux-common'; import axios from 'axios'; +import { ArrayOfKASP, ISO4217_Map } from './TypeUtils'; /** * Signs the given request and adds the related headers to it. @@ -289,6 +290,392 @@ function getAmzDateString(date: Date): string { ); } +export const iSO4217_AlphaArray: (keyof ISO4217_Map)[] = [ + 'AED', + 'AFN', + 'ALL', + 'AMD', + 'ANG', + 'AOA', + 'ARS', + 'AUD', + 'AWG', + 'AZN', + 'BAM', + 'BBD', + 'BDT', + 'BGN', + 'BHD', + 'BIF', + 'BMD', + 'BND', + 'BOB', + 'BOV', + 'BRL', + 'BSD', + 'BTN', + 'BWP', + 'BYN', + 'BZD', + 'CAD', + 'CDF', + 'CHE', + 'CHF', + 'CHW', + 'CLF', + 'CLP', + 'CNY', + 'COP', + 'COU', + 'CRC', + 'CUC', + 'CUP', + 'CVE', + 'CZK', + 'DJF', + 'DKK', + 'DOP', + 'DZD', + 'EGP', + 'ERN', + 'ETB', + 'EUR', + 'FJD', + 'FKP', + 'GBP', + 'GEL', + 'GHS', + 'GIP', + 'GMD', + 'GNF', + 'GTQ', + 'GYD', + 'HKD', + 'HNL', + 'HTG', + 'HUF', + 'IDR', + 'ILS', + 'INR', + 'IQD', + 'IRR', + 'ISK', + 'JMD', + 'JOD', + 'JPY', + 'KES', + 'KGS', + 'KHR', + 'KMF', + 'KPW', + 'KRW', + 'KWD', + 'KYD', + 'KZT', + 'LAK', + 'LBP', + 'LKR', + 'LRD', + 'LSL', + 'LYD', + 'MAD', + 'MDL', + 'MGA', + 'MKD', + 'MMK', + 'MNT', + 'MOP', + 'MRU', + 'MUR', + 'MVR', + 'MWK', + 'MXN', + 'MXV', + 'MYR', + 'MZN', + 'NAD', + 'NGN', + 'NIO', + 'NOK', + 'NPR', + 'NZD', + 'OMR', + 'PAB', + 'PEN', + 'PGK', + 'PHP', + 'PKR', + 'PLN', + 'PYG', + 'QAR', + 'RON', + 'RSD', + 'RUB', + 'RWF', + 'SAR', + 'SBD', + 'SCR', + 'SDG', + 'SEK', + 'SGD', + 'SHP', + 'SLE', + 'SOS', + 'SRD', + 'SSP', + 'STN', + 'SVC', + 'SYP', + 'SZL', + 'THB', + 'TJS', + 'TMT', + 'TND', + 'TOP', + 'TRY', + 'TTD', + 'TWD', + 'TZS', + 'UAH', + 'UGX', + 'USD', + 'USN', + 'UYI', + 'UYU', + 'UYW', + 'UZS', + 'VED', + 'VES', + 'VND', + 'VUV', + 'WST', + 'XAF', + 'XAG', + 'XAU', + 'XBA', + 'XBB', + 'XBC', + 'XBD', + 'XCD', + 'XDR', + 'XOF', + 'XPD', + 'XPF', + 'XPT', + 'XSU', + 'XTS', + 'XUA', + 'XXX', + 'YER', + 'ZAR', + 'ZMW', + 'ZWG', + 'ZWL', +]; + +const ISO4217_Map: ArrayOfKASP = [ + ['AED', 784, 2], + ['AFN', 971, 2], + ['ALL', 8, 2], + ['AMD', 51, 2], + ['ANG', 532, 2], + ['AOA', 973, 2], + ['ARS', 32, 2], + ['AUD', 36, 2], + ['AWG', 533, 2], + ['AZN', 944, 2], + ['BAM', 977, 2], + ['BBD', 52, 2], + ['BDT', 50, 2], + ['BGN', 975, 2], + ['BHD', 48, 3], + ['BIF', 108, 0], + ['BMD', 60, 2], + ['BND', 96, 2], + ['BOB', 68, 2], + ['BOV', 984, 2], + ['BRL', 986, 2], + ['BSD', 44, 2], + ['BTN', 64, 2], + ['BWP', 72, 2], + ['BYN', 933, 2], + ['BZD', 84, 2], + ['CAD', 124, 2], + ['CDF', 976, 2], + ['CHE', 947, 2], + ['CHF', 756, 2], + ['CHW', 948, 2], + ['CLF', 990, 4], + ['CLP', 152, 0], + ['CNY', 156, 2], + ['COP', 170, 2], + ['COU', 970, 2], + ['CRC', 188, 2], + ['CUC', 931, 2], + ['CUP', 192, 2], + ['CVE', 132, 2], + ['CZK', 203, 2], + ['DJF', 262, 0], + ['DKK', 208, 2], + ['DOP', 214, 2], + ['DZD', 12, 2], + ['EGP', 818, 2], + ['ERN', 232, 2], + ['ETB', 230, 2], + ['EUR', 978, 2], + ['FJD', 242, 2], + ['FKP', 238, 2], + ['GBP', 826, 2], + ['GEL', 981, 2], + ['GHS', 936, 2], + ['GIP', 292, 2], + ['GMD', 270, 2], + ['GNF', 324, 0], + ['GTQ', 320, 2], + ['GYD', 328, 2], + ['HKD', 344, 2], + ['HNL', 340, 2], + ['HTG', 332, 2], + ['HUF', 348, 2], + ['IDR', 360, 2], + ['ILS', 376, 2], + ['INR', 356, 2], + ['IQD', 368, 3], + ['IRR', 364, 2], + ['ISK', 352, 0], + ['JMD', 388, 2], + ['JOD', 400, 3], + ['JPY', 392, 0], + ['KES', 404, 2], + ['KGS', 417, 2], + ['KHR', 116, 2], + ['KMF', 174, 0], + ['KPW', 408, 2], + ['KRW', 410, 0], + ['KWD', 414, 3], + ['KYD', 136, 2], + ['KZT', 398, 2], + ['LAK', 418, 2], + ['LBP', 422, 2], + ['LKR', 144, 2], + ['LRD', 430, 2], + ['LSL', 426, 2], + ['LYD', 434, 3], + ['MAD', 504, 2], + ['MDL', 498, 2], + ['MGA', 969, 2], + ['MKD', 807, 2], + ['MMK', 104, 2], + ['MNT', 496, 2], + ['MOP', 446, 2], + ['MRU', 929, 2], + ['MUR', 480, 2], + ['MVR', 462, 2], + ['MWK', 454, 2], + ['MXN', 484, 2], + ['MXV', 979, 2], + ['MYR', 458, 2], + ['MZN', 943, 2], + ['NAD', 516, 2], + ['NGN', 566, 2], + ['NIO', 558, 2], + ['NOK', 578, 2], + ['NPR', 524, 2], + ['NZD', 554, 2], + ['OMR', 512, 3], + ['PAB', 590, 2], + ['PEN', 604, 2], + ['PGK', 598, 2], + ['PHP', 608, 2], + ['PKR', 586, 2], + ['PLN', 985, 2], + ['PYG', 600, 0], + ['QAR', 634, 2], + ['RON', 946, 2], + ['RSD', 941, 2], + ['RUB', 643, 2], + ['RWF', 646, 0], + ['SAR', 682, 2], + ['SBD', 90, 2], + ['SCR', 690, 2], + ['SDG', 938, 2], + ['SEK', 752, 2], + ['SGD', 702, 2], + ['SHP', 654, 2], + ['SLE', 925, 2], + ['SOS', 706, 2], + ['SRD', 968, 2], + ['SSP', 728, 2], + ['STN', 930, 2], + ['SVC', 222, 2], + ['SYP', 760, 2], + ['SZL', 748, 2], + ['THB', 764, 2], + ['TJS', 972, 2], + ['TMT', 934, 2], + ['TND', 788, 3], + ['TOP', 776, 2], + ['TRY', 949, 2], + ['TTD', 780, 2], + ['TWD', 901, 2], + ['TZS', 834, 2], + ['UAH', 980, 2], + ['UGX', 800, 0], + ['USD', 840, 2], + ['USN', 997, 2], + ['UYI', 940, 0], + ['UYU', 858, 2], + ['UYW', 927, 4], + ['UZS', 860, 2], + ['VED', 926, 2], + ['VES', 928, 2], + ['VND', 704, 0], + ['VUV', 548, 0], + ['WST', 882, 2], + ['XAF', 950, 0], + ['XAG', 961, null], + ['XAU', 959, null], + ['XBA', 955, null], + ['XBB', 956, null], + ['XBC', 957, null], + ['XBD', 958, null], + ['XCD', 951, 2], + ['XDR', 960, null], + ['XOF', 952, 0], + ['XPD', 964, null], + ['XPF', 953, 0], + ['XPT', 962, null], + ['XSU', 994, null], + ['XTS', 963, null], + ['XUA', 965, null], + ['XXX', 999, null], + ['YER', 886, 2], + ['ZAR', 710, 2], + ['ZMW', 967, 2], + ['ZWG', 924, 2], + ['ZWL', 932, 2], +]; + +/** + * Gets the ISO 4217 currency meta for the given currency name + * or numeric code + * @important + * ! Be sure to validate that the implementing service supports + * ! the currency as well as the minor unit count (if applicable) + * @param query The currency name or numeric code + */ +export function getISO4217CurrencyCode( + query: keyof ISO4217_Map | ISO4217_Map[keyof ISO4217_Map]['n'] +): [ + keyof ISO4217_Map, + ISO4217_Map[keyof ISO4217_Map]['n'], + ISO4217_Map[keyof ISO4217_Map]['mU'] +] { + return ISO4217_Map.find( + (v) => v[typeof query == 'string' ? 0 : 1] === query + ); +} + /** * Parses the given string of instance names into an array of instance names. * @param instances The names of the instances. @@ -314,6 +701,107 @@ export function cleanupObject(obj: T): Partial { ) as Partial; } +// /** +// * Deep clones and concurrently freezes within the same iteration the provided object (record/array). +// * @param obj The object to clone and freeze recursively. +// */ +// export function deepCloneFreeze | Array>( +// obj: T +// ): Readonly { +// const returnObj = (Array.isArray(obj) ? [] : {}) as T; +// for (const key in obj) { +// if (obj.hasOwnProperty(key)) { +// const value = obj[key]; +// if ( +// typeof value === 'object' && +// value !== null && +// !(value instanceof Date) +// ) { +// returnObj[key] = deepCloneFreeze(value) as T[Extract< +// keyof T, +// string +// >]; +// } else { +// returnObj[key] = value; +// } +// } +// } +// return Object.freeze(returnObj); +// } + +// /** +// * Provides a function that can be used to build a paginate/effect function for the given array. +// * @param arr The array to paginate. +// * @param effectEach A function used within map to effect each element. +// * @returns +// */ +// export function paginationProviderOf( +// arr: T[], +// effectEach?: (item: T) => PromiseOrValue +// ) { +// return async (limit: number, offset: number) => { +// const content = arr.slice(offset, offset + limit); +// if (!effectEach || content.length < 1) return []; +// return await Promise.all(content.map((X) => effectEach(X))); +// }; +// } + +// ! Implementation is WIP +// /** +// * Generates a custom asymmetric matcher used by jest. +// * * (Similar to expect.any) +// * @param asymmetricMatcher The function to run on the value to indicate whether or not its as expected +// * @param toString The string provider of the expected values state/type +// * @param getExpectedType The string provider of the expected values type(s) +// */ +// export function jestCustomAsymmetricMatcher( +// asymmetricMatcher: (value: any) => boolean, +// toAsymmetricMatcher: () => string, +// toString?: () => string, +// getExpectedType?: () => string +// ) { +// return { +// $$typeof: Symbol.for('jest.asymmetricMatcher'), +// asymmetricMatcher, +// toAsymmetricMatcher, +// toString, +// getExpectedType, +// }; +// } + +/** + * A function which returns true if param val is found to be type / instance of any specified in param constructorArray + * @param val The value whose type / instance to check the presence of in constructorArray + * @param constructorArray The array which contains the allowed types / instances for parameter val + */ +export function isOfXType( + val: T, + constructorArray: Array +): boolean { + return constructorArray.some((constructor) => { + return constructor === String || + constructor === Number || + constructor === Boolean || + constructor === Symbol || + constructor === BigInt || + constructor === undefined + ? typeof val === (constructor?.name?.toLowerCase() ?? 'undefined') + : constructor === null + ? val === null + : val instanceof constructor; + }); +} + +/** + * Clones the given record or returns null if the record is undefined. + * @param record The record to clone. + */ +export function cloneDeepNull>( + record: T | undefined +) { + return record ? _.cloneDeep(record) : null; +} + /** * Tries to parse the given JSON string into a JavaScript Value. * @param json The JSON to parse. @@ -332,6 +820,45 @@ export function tryParseJson(json: string): JsonParseResult { } } +interface ScopedErrorConfig { + /** The scope of the function. E.g. [ClassName, FunctionName] */ + scope: Array | string; + /** The message to log if an error occurs. */ + errMsg: string; +} + +export function scopedError(errConfig: ScopedErrorConfig, err: string) { + console.error( + Array.isArray(errConfig.scope) + ? errConfig.scope.map((s) => `[${s}]`).join(' ') + : `[${errConfig.scope}]`, + errConfig.errMsg, + err + ); +} + +interface ScopedTryConfig extends ScopedErrorConfig { + /** The value to return if an error occurs. */ + returnOnError?: E; +} + +/** + * Attempts to run the given function and logs an error if it fails with a standard error format which includes the scope and message. + * @param fn The function to run. + * @param errConfig The configuration for proper logging and return value if an error occurs. + */ +export async function tryScope( + fn: () => T, + errConfig: ScopedTryConfig +): Promise { + try { + return await fn(); + } catch (err) { + scopedError(errConfig, err); + return errConfig.returnOnError; + } +} + export type JsonParseResult = JsonParseSuccess | JsonParseFailure; export interface JsonParseSuccess { diff --git a/src/aux-records/XpController.spec.ts b/src/aux-records/XpController.spec.ts new file mode 100644 index 0000000000..4cb5063840 --- /dev/null +++ b/src/aux-records/XpController.spec.ts @@ -0,0 +1,236 @@ +import { createTestControllers, createTestUser } from './TestUtils'; +import { MemoryAuthMessenger } from './MemoryAuthMessenger'; +import { AuthController } from './AuthController'; +import { MemoryStore } from './MemoryStore'; +import { XpController } from '../aux-records/XpController'; +import { XpAccount, XpInvoice, XpUser } from './XpStore'; +import { NotNullOrOptional, PromiseT } from './TypeUtils'; +import { v4 as uuid } from 'uuid'; + +jest.mock('uuid'); +const uuidMock: jest.Mock = uuid; + +interface UniqueConfig { + /** A test scope name to be used in the uuid for easy debug */ + name: string; + /** An optional index to start the unique count at */ + c?: number; +} + +const unique = (uConf: UniqueConfig) => { + uConf.c = typeof uConf?.c === 'undefined' ? 0 : uConf.c + 1; + return `unique-gen-${uConf.name}-${uConf.c}`; +}; + +const uniqueWithMock = (uConf: UniqueConfig) => { + const v = unique(uConf); + uuidMock.mockReturnValueOnce(v); + return v; +}; + +const manyUniqueWithMock = (uConf: UniqueConfig, n: number) => + Array.from({ length: n }, (_, i) => uniqueWithMock(uConf)); + +/** + * XpController tests + */ +describe('XpController', () => { + //* BOCS: Init services + let services: ReturnType; + let xpController: XpController; + let memoryStore: MemoryStore; + let authController: AuthController; + let authMessenger: MemoryAuthMessenger; + let _testDateNow: number; + //* EOCS: Init services + + //* BOCS: Mocking Date.now + /** + * Stores original function reference for restoration after tests + */ + const dateNowRef = Date.now; + let nowMock: jest.Mock; + //* EOC: Mocking Date.now + + /** + * (Re-)Initialize services required for tests + */ + const initServices = () => { + services = createTestControllers(); + xpController = new XpController({ + xpStore: services.store, + authController: services.auth, + authStore: services.store, + }); + memoryStore = services.store; + authController = services.auth; + authMessenger = services.authMessenger; + }; + + /** + * Runs before all tests (once) + */ + beforeAll(async () => { + initServices(); + }); + + /** + * Runs before each test + */ + beforeEach(() => { + initServices(); + _testDateNow = dateNowRef(); + nowMock = Date.now = jest.fn(); + nowMock.mockReturnValue(_testDateNow); + }); + + /** + * Runs after each test + */ + afterEach(() => { + //* Reset the uuid mock to ensure that it is clean for the next test + uuidMock.mockReset(); + nowMock.mockReset(); + Date.now = dateNowRef; + }); + + //* Test case for the XpController class + it('should be defined', () => { + expect(XpController).toBeTruthy(); + expect(xpController).toBeTruthy(); + }); + + /** + * Successful getXpUser invocations across the board should be indicative of— + * successful XP user initialization as well as retrieval + */ + describe('getXpUser', () => { + //* An auth user for use in testing getXpUser + let _authUser: PromiseT>; + + //* The expected result of a successful getXpUser call + let _xpUser: XpUser; + + //* Unique function config + const uConf = { name: 'getXpUser' }; + + beforeEach(async () => { + const [_, accountId, id] = manyUniqueWithMock(uConf, 3); + + _authUser = await createTestUser( + { + auth: authController, + authMessenger: authMessenger, + }, + 'xp.test@localhost' + ); + + const xpAccount: XpAccount = { + id: accountId, + currency: 'USD', + closedTimeMs: null, + createdAtMs: _testDateNow, + updatedAtMs: _testDateNow, + }; + + const xpUser: XpUser = { + id, + userId: _authUser.userId, + accountId: xpAccount.id, + requestedRate: null, + createdAtMs: _testDateNow, + updatedAtMs: _testDateNow, + }; + + await memoryStore.saveXpUserWithAccount(xpUser, xpAccount); + + _xpUser = xpUser; + + uuidMock.mockReset(); + }); + + it('should create and return a new xp user when given a valid userId whose respective auth user does not have an xp user identity', async () => { + const [userId, accountId, id] = manyUniqueWithMock(uConf, 3); + + const newAuthUser = await createTestUser( + { + auth: authController, + authMessenger: authMessenger, + }, + `xp.test.newAuthUser0@localhost` + ); + + expect( + await xpController.getXpUser({ + userId: newAuthUser.userId, + }) + ).toEqual({ + success: true, + user: { + id, + userId, + accountId, + requestedRate: null, + createdAtMs: _testDateNow, + updatedAtMs: _testDateNow, + }, + }); + }); + + it('should get an xp user by auth id', async () => { + expect( + await xpController.getXpUser({ + userId: _authUser.userId, + }) + ).toEqual({ + success: true, + user: _xpUser, + }); + }); + + it('should get an xp user by xp id', async () => { + expect( + await xpController.getXpUser({ + xpId: _xpUser.id, + }) + ).toEqual({ + success: true, + user: _xpUser, + }); + }); + + it('should fail to get an xp user due to use of multiple identifiers', async () => { + const xpUser = await xpController.getXpUser({ + userId: _authUser.userId, + xpId: 'some-id', + }); + expect(xpUser).toEqual({ + success: false, + errorCode: 'invalid_request', + errorMessage: expect.any(String), + }); + }); + + it('should fail to get an xp user by auth id due to user not found', async () => { + const xpUser = await xpController.getXpUser({ + userId: 'non-existent-id', + }); + expect(xpUser).toEqual({ + success: false, + errorCode: 'user_not_found', + errorMessage: expect.any(String), + }); + }); + + it('should fail to get an xp user by xp id due to user not found', async () => { + const xpUser = await xpController.getXpUser({ + xpId: 'non-existent-id', + }); + expect(xpUser).toEqual({ + success: false, + errorCode: 'user_not_found', + errorMessage: expect.any(String), + }); + }); + }); +}); diff --git a/src/aux-records/XpController.ts b/src/aux-records/XpController.ts new file mode 100644 index 0000000000..aea61ead61 --- /dev/null +++ b/src/aux-records/XpController.ts @@ -0,0 +1,175 @@ +import { XpAccount, XpContract, XpInvoice, XpStore, XpUser } from './XpStore'; +import { AuthController } from './AuthController'; +import { AuthStore, AuthUser } from './AuthStore'; +import { v4 as uuid } from 'uuid'; +import { ISO4217_Map, SuccessResult } from './TypeUtils'; +import { KnownErrorCodes } from '@casual-simulation/aux-common'; +import { traced } from './tracing/TracingDecorators'; +import { iSO4217_AlphaArray, tryScope } from './Utils'; + +interface XpConfig { + xpStore: XpStore; + authController: AuthController; + authStore: AuthStore; +} + +const TRACE_NAME = 'XpController'; + +/** + * Defines a class that controls an auth users relationship with the XP "system". + */ +export class XpController { + private _auth: AuthController; + private _authStore: AuthStore; + private _xpStore: XpStore; + + constructor(config: XpConfig) { + this._auth = config.authController; + this._authStore = config.authStore; + this._xpStore = config.xpStore; + } + + /** + * Generate an account (user or contract) configured for the given currency + * @param currency The currency to configure the account for (default: USD) + */ + private _generateAccount(currency: keyof ISO4217_Map = 'USD'): XpAccount { + return { + id: uuid(), + currency, + createdAtMs: Date.now(), + updatedAtMs: Date.now(), + closedTimeMs: null, + }; + } + + /** + * Create an XP user for the given auth user + * @param authUserId The ID of the auth user to create an XP user for + */ + @traced(TRACE_NAME) + private async _createXpUser( + authUserId: string + ): Promise { + return await tryScope( + async () => { + const authUser = await this._authStore.findUser(authUserId); + if (!authUser) { + return { + success: false, + errorCode: 'user_not_found', + errorMessage: 'The user was not found.', + }; + } + + const account: XpAccount = this._generateAccount(); + const user: XpUser = { + id: uuid(), + userId: authUserId, + accountId: account.id, + requestedRate: null, + createdAtMs: Date.now(), + updatedAtMs: Date.now(), + }; + await this._xpStore.saveXpUserWithAccount(user, account); + return { success: true, user }; + }, + { + scope: [TRACE_NAME, this._createXpUser.name], + errMsg: 'An error occurred while creating the user or associated account.', + returnOnError: { + success: false, + errorCode: 'server_error', + errorMessage: + 'An error occurred while creating the user or associated account.', + }, + } + ); + } + + /** + * Get an Xp user's meta data (Xp meta associated with an auth user) + * Creates an Xp user for the auth user if one does not exist + */ + async getXpUser(id: GetXpUserById): Promise { + return await tryScope( + async () => { + let user = + (await this._xpStore[ + `getXpUserBy${id.userId ? 'Auth' : ''}Id` + ](id.userId ?? id.xpId)) ?? null; + if (id.userId !== undefined && id.xpId !== undefined) + return { + success: false, + errorCode: 'invalid_request', + errorMessage: + 'Cannot use multiple identifiers to get a user.', + }; + if (!user && id.userId) { + const result = await this._createXpUser(id.userId); + if (result.success) { + user = result.user; + } else return result; + } + return user + ? { success: true, user } + : { + success: false, + errorCode: 'user_not_found', + errorMessage: 'The user was not found.', + }; + }, + { + scope: [TRACE_NAME, this.getXpUser.name], + errMsg: 'An error occurred while getting the user.', + returnOnError: { + success: false, + errorCode: 'server_error', + errorMessage: 'An error occurred while getting the user.', + }, + } + ); + } +} +export interface GetXpUserById { + /** The auth Id of the xp user to get (mutually exclusive with xpId) */ + userId?: AuthUser['id']; + /** The xp Id of the xp user to get (mutually exclusive with userId) */ + xpId?: XpUser['id']; +} + +export type FailedResult = SuccessResult< + false, + { errorCode: KnownErrorCodes; errorMessage: string } +>; + +export type CreateXpUserResultSuccess = SuccessResult; +export type CreateXpUserResultFailure = FailedResult; +export type CreateXpUserResult = + | CreateXpUserResultSuccess + | CreateXpUserResultFailure; + +export type GetXpUserResultSuccess = SuccessResult; +export type GetXpUserResultFailure = FailedResult; +export type GetXpUserResult = GetXpUserResultSuccess | GetXpUserResultFailure; + +export type CreateContractResultSuccess = SuccessResult< + true, + { + contract: XpContract; + account: XpAccount; + } +>; +export type CreateContractResultFailure = FailedResult; +export type CreateContractResult = + | CreateContractResultSuccess + | CreateContractResultFailure; + +export type GetContractResultSuccess = SuccessResult< + true, + { contract: XpContract } +>; +export type GetContractResultFailure = FailedResult; +export type GetContractResult = + | GetContractResultSuccess + | GetContractResultFailure; diff --git a/src/aux-records/XpStore.ts b/src/aux-records/XpStore.ts new file mode 100644 index 0000000000..28aaa8538f --- /dev/null +++ b/src/aux-records/XpStore.ts @@ -0,0 +1,210 @@ +import { AuthUser } from './AuthStore'; +import { + SuccessResult, + DateMS, + GenericTimeKeys, + GigQty, + ISO4217_Map, + CurrencySFU, +} from './TypeUtils'; + +export type XpStore = { + /** + * Save an xp user with an associated account + * @param user The user to create + * @param account The account to create + */ + saveXpUserWithAccount: ( + user: XpUser, + account: XpAccount + ) => Promise; + + /** + * Get an xp account by its id + * @param accountId The id of the account to get + */ + getXpAccount: (accountId: XpAccount['id']) => Promise; + + /** + * Get an xp account entry by its id + * @param entryId The id of the entry to get + */ + getXpAccountEntry: ( + entryId: XpAccountEntry['id'] + ) => Promise; + + /** + * Get an xp contract by its id + * @param contractId The id of the contract to get + */ + getXpContract: (contractId: XpContract['id']) => Promise; + + /** + * Get an xp invoice by its id + * @param invoiceId The id of the invoice to get + */ + getXpInvoice: (invoiceId: XpInvoice['id']) => Promise; + + /** + * Get an xp user by their auth id + * @param id The auth user id of the user to get + */ + getXpUserByAuthId: (id: AuthUser['id']) => Promise; + + /** + * Get an xp user by their xp id + * @param id The xp user id of the xp user to get + */ + getXpUserById: (id: XpUser['id']) => Promise; + + /** + * Save an xp account for a user or contract + * @param associationId The id of the xp user or contract to create an account for + * @param account The account to save + */ + saveXpAccount( + associationId: T extends 'user' ? XpUser['id'] : XpContract['id'], + account: XpAccount + ): Promise; + + /** + * Save an xp contract with an associated account + * @param contract The contract to save + * @param account The account to save + */ + saveXpContract: ( + contract: XpContract, + account: XpAccount | null + ) => Promise; + + /** + * Save an xp user associated with the given auth user + * @param id The id of the xp user to create an account for (uuid) not the auth user id + * @param user The meta data to associate with the user + */ + saveXpUser: (id: XpUser['id'], user: XpUser) => Promise; + + /** + * Performs a transaction consisting of multiple entries + * @param entries The entries to perform the transaction with + */ + // inTransaction: () => Promise; // TODO: Implement this +}; + +/** + * An extensible model for all models in the xp system + */ +export interface ModelBase extends GenericTimeKeys { + /** The unique id of the model item */ + id: string; +} + +/** + * Data to be returned within get invocation results on each model + */ +export interface XpUser extends ModelBase { + /** The id of the associated account */ + accountId: XpAccount['id']; + /** The rate at which the user is requesting payment (null if not yet specified) */ + requestedRate: CurrencySFU | null; + /** The users unique id from the Auth system */ + userId: string; +} + +/** + * An account in the xp system + * * Ties the— and enables tracking of— the flow of money between entities + * * Hosts entries that represent the addition or withdrawal of money + */ +export interface XpAccount extends ModelBase { + /** The time at which (if) the account was closed */ + closedTimeMs: DateMS | null; + /** The currency of the account */ + currency: keyof ISO4217_Map; +} + +/** + * Mimics a "Smart Contract" (crypto/blockchain) data model for the xp system + */ +export interface XpContract extends ModelBase { + /** The id of the account associated with the contract */ + accountId: XpAccount['id']; + /** A description of the contract, may contain useful query meta */ + description: string | null; + /** The id of the user holding the contract */ + holdingUserId: XpUser['id'] | null; + /** The id of the user issuing the contract */ + issuerUserId: XpUser['id']; + /** The rate at which the entirety of the contract is worth */ + rate: CurrencySFU; + /** An amount of money associated with a contract offer (would be the amount in the contract account if accepted) */ + offeredWorth: CurrencySFU; + /** The status of the contract */ + status: 'open' | 'draft' | 'closed'; +} + +/** + * Represents an entry in an xp account + * * Used to track the flow of money in the xp system (ledger) + */ +export interface XpAccountEntry extends ModelBase { + /** The id of the account associated with the account entry */ + accountId: XpAccount['id']; + /** The amount of money to be contributed to the account, negative for withdrawals */ + amount: number; + /** The new balance of the account after the entry was made */ + balance: number; + /** A note for the entry */ + note: string | null; + /** The id of the system event associated with the account entry */ + systemEventId: XpSystemEvent['id']; + /** The time at which the entry was made in the real world */ + timeMs: DateMS; + /** An id of a transaction, used to group entries together */ + transactionId: string; +} + +/** + * Represents an adjustment event in the xp system (audit tool) + */ +export interface XpSystemEventAdjustment extends ModelBase {} + +/** + * Represents an event in the xp system (audit tool) + */ +export interface XpSystemEvent extends ModelBase { + /** The XpSystemEventAdjustment id of the adjusting adjustment event */ + adjustingEventId: XpSystemEventAdjustment['id'] | null; + /** The XpSystemEventAdjustment id of the adjusted adjustment event */ + adjusterEventId: XpSystemEventAdjustment['id'] | null; + /** JSON data related to the event, which can be of any return type */ + data: any; + /** The time the event occurred in the real world */ + timeMs: DateMS; + /** The string literal representation of a type of system event */ + type: + | 'adjustment' + | 'create_invoice' + | 'create_contract' + | 'create_account_entry'; + /** The id of the user who performed the event (null if system caused) */ + xpUserId: XpUser['id'] | null; +} + +/** + * Represents an invoice for a contract + */ +export interface XpInvoice extends ModelBase { + /** The id of the contract the invoice is made for */ + contractId: XpContract['id']; + /** The quantity of gigs being invoiced for */ + amount: GigQty; + /** The status of the invoice */ + status: 'open' | 'paid' | 'void'; + /** The reason (if any) the invoice was made void */ + voidReason: 'rejected' | 'cancelled' | null; + /** An id of a transaction associated with the invoice(s) */ + transactionId: string; + /** A note for the invoice */ + note: string | null; +} diff --git a/src/aux-records/__snapshots__/RecordsServer.spec.ts.snap b/src/aux-records/__snapshots__/RecordsServer.spec.ts.snap index 6ffa811d68..611f0a35d7 100644 --- a/src/aux-records/__snapshots__/RecordsServer.spec.ts.snap +++ b/src/aux-records/__snapshots__/RecordsServer.spec.ts.snap @@ -1940,6 +1940,143 @@ Object { "name": "getNotificationsApplicationServerKey", "origins": "api", }, + Object { + "http": Object { + "method": "GET", + "path": "/api/v2/xp/user", + }, + "inputs": Object { + "schema": Object { + "userId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + "xpId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + }, + "type": "object", + }, + "name": "getXpUserMeta", + "origins": "api", + }, + Object { + "http": Object { + "method": "POST", + "path": "/api/v2/xp/contract", + }, + "inputs": Object { + "schema": Object { + "contract": Object { + "schema": Object { + "accountCurrency": Object { + "optional": true, + "type": "string", + }, + "contractedUserId": Object { + "nullable": true, + "optional": true, + "schema": Object { + "userId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + "xpId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + }, + "type": "object", + }, + "description": Object { + "optional": true, + "type": "string", + }, + "gigRate": Object { + "type": "number", + }, + "gigs": Object { + "type": "number", + }, + "status": Object { + "options": Array [ + Object { + "type": "literal", + "value": "open", + }, + Object { + "type": "literal", + "value": "draft", + }, + ], + "type": "union", + }, + }, + "type": "object", + }, + }, + "type": "object", + }, + "name": "createXpContract", + "origins": "api", + }, + Object { + "http": Object { + "method": "PUT", + "path": "/api/v2/xp/contract", + }, + "inputs": Object { + "schema": Object { + "contractId": Object { + "type": "string", + }, + "newDescription": Object { + "optional": true, + "type": "string", + }, + "newStatus": Object { + "options": Array [ + Object { + "type": "literal", + "value": "open", + }, + Object { + "type": "literal", + "value": "draft", + }, + Object { + "type": "literal", + "value": "closed", + }, + ], + "type": "union", + }, + "receivingUserId": Object { + "schema": Object { + "userId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + "xpId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + }, + "type": "object", + }, + }, + "type": "object", + }, + "name": "updateXpContract", + "origins": "api", + }, Object { "http": Object { "method": "GET", @@ -5784,6 +5921,143 @@ Object { "name": "getNotificationsApplicationServerKey", "origins": "api", }, + Object { + "http": Object { + "method": "GET", + "path": "/api/v2/xp/user", + }, + "inputs": Object { + "schema": Object { + "userId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + "xpId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + }, + "type": "object", + }, + "name": "getXpUserMeta", + "origins": "api", + }, + Object { + "http": Object { + "method": "POST", + "path": "/api/v2/xp/contract", + }, + "inputs": Object { + "schema": Object { + "contract": Object { + "schema": Object { + "accountCurrency": Object { + "optional": true, + "type": "string", + }, + "contractedUserId": Object { + "nullable": true, + "optional": true, + "schema": Object { + "userId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + "xpId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + }, + "type": "object", + }, + "description": Object { + "optional": true, + "type": "string", + }, + "gigRate": Object { + "type": "number", + }, + "gigs": Object { + "type": "number", + }, + "status": Object { + "options": Array [ + Object { + "type": "literal", + "value": "open", + }, + Object { + "type": "literal", + "value": "draft", + }, + ], + "type": "union", + }, + }, + "type": "object", + }, + }, + "type": "object", + }, + "name": "createXpContract", + "origins": "api", + }, + Object { + "http": Object { + "method": "PUT", + "path": "/api/v2/xp/contract", + }, + "inputs": Object { + "schema": Object { + "contractId": Object { + "type": "string", + }, + "newDescription": Object { + "optional": true, + "type": "string", + }, + "newStatus": Object { + "options": Array [ + Object { + "type": "literal", + "value": "open", + }, + Object { + "type": "literal", + "value": "draft", + }, + Object { + "type": "literal", + "value": "closed", + }, + ], + "type": "union", + }, + "receivingUserId": Object { + "schema": Object { + "userId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + "xpId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + }, + "type": "object", + }, + }, + "type": "object", + }, + "name": "updateXpContract", + "origins": "api", + }, Object { "http": Object { "method": "GET", diff --git a/src/aux-records/index.ts b/src/aux-records/index.ts index bd13f9af5a..023bc9a33f 100644 --- a/src/aux-records/index.ts +++ b/src/aux-records/index.ts @@ -62,3 +62,6 @@ export * from './ServerConfig'; export * from './webhooks'; export * from './notifications'; + +export * from './XpController'; +export * from './XpStore'; diff --git a/src/aux-records/websockets/MemoryLockStore.ts b/src/aux-records/websockets/MemoryLockStore.ts new file mode 100644 index 0000000000..2885e27200 --- /dev/null +++ b/src/aux-records/websockets/MemoryLockStore.ts @@ -0,0 +1,25 @@ +/** + * Represents a memory lock store + * * This is used to store and enforce locks in memory + */ +export interface MemoryLockStore { + /** + * The locks that are currently being held. + * * The key is the id of the lock and the value is the time at which the lock will be released + * ! Implementation advice (private _locks map) + * private _locks: Map = new Map(); + */ + + /** + * Acquire a lock for/with the given id + * @param id The id to acquire the lock for + * @param timeout The amount of time to wait before the lock is released + * @returns + * If successful, returns a function that will release the lock when called. + * If unsuccessful, returns null. + */ + acquireLock( + id: string, + timeout: number + ): Promise<() => Promise | null>; +} diff --git a/src/aux-runtime/runtime/AuxLibrary.ts b/src/aux-runtime/runtime/AuxLibrary.ts index 03a8c4c7fe..0a4fb4c7d2 100644 --- a/src/aux-runtime/runtime/AuxLibrary.ts +++ b/src/aux-runtime/runtime/AuxLibrary.ts @@ -324,6 +324,7 @@ import { SendNotificationOptions, listNotificationSubscriptions as calcListNotificationSubscriptions, listUserNotificationSubscriptions as calcListUserNotificationSubscriptions, + getXpUserMeta, } from './RecordsEvents'; import { sortBy, @@ -3493,6 +3494,19 @@ export function createDefaultLibrary(context: AuxGlobalContext) { hook: makeMockableFunction(webhook, 'web.hook'), }, + xp: { + getUserMeta: async function ( + by?: {} | string, + options: RecordActionOptions = {} + ) { + const task = context.createTask(); + return await addAsyncResultAction( + task, + getXpUserMeta(by, options, task.taskId) + ); + }, + }, + analytics: { recordEvent: analyticsRecordEvent, }, diff --git a/src/aux-runtime/runtime/AuxLibraryDefinitions.def b/src/aux-runtime/runtime/AuxLibraryDefinitions.def index 0e45848906..6ea74bf3c2 100644 --- a/src/aux-runtime/runtime/AuxLibraryDefinitions.def +++ b/src/aux-runtime/runtime/AuxLibraryDefinitions.def @@ -1,4 +1,3 @@ - // Preact Hooks Types type Inputs = ReadonlyArray; @@ -6,25 +5,24 @@ export type StateUpdater = (value: S | ((prevState: S) => S)) => void; export type Reducer = (prevState: S, action: A) => S; interface Ref { - readonly current: T | null; + readonly current: T | null; } interface MutableRef { - current: T; + current: T; } // End Preact Hooks Types type EffectCallback = () => void | (() => void); -type MaskFunc any)> = { +type MaskFunc any> = { (...args: Parameters): ReturnType; /** * Masks this function so that it can return a value when called with the specified parameters. */ mask(...args: Parameters): MaskedFunction; -} - +}; export interface MaskedFunction { /** @@ -70,7 +68,7 @@ export interface AuxVersion { /** * Gets the player mode of this CasualOS version. - * + * * - "player" indicates that the instance has been configured for experiencing AUXes. * - "builder" indicates that the instance has been configured for building AUXes. */ @@ -342,7 +340,6 @@ declare interface UpdateBotAction extends Action { update: Partial; } - /** * Defines the set of options required for creating a certificate. */ @@ -368,7 +365,7 @@ export interface CreateCertificateOptions { */ export interface CreateCertificateAction extends AsyncAction, - CreateCertificateOptions { + CreateCertificateOptions { type: 'create_certificate'; } @@ -495,7 +492,6 @@ declare interface ReplaceDragBotAction extends Action { bot: Bot | BotTags; } - /** * An event that is used to run a shell script. */ @@ -580,25 +576,25 @@ export interface FocusOnOptions { * The tag that should be focused. * Only supported by the system portal. */ - tag?: string; - - /** - * The space of the tag that should be focused. - * Only supported by the system portal. - */ - space?: string; - - /** - * The line number that should be focued. - * Only supported by the system portal. - */ - lineNumber?: number; - - /** - * The column number that should be focused. - * Only supported by the system portal. - */ - columnNumber?: number; + tag?: string; + + /** + * The space of the tag that should be focused. + * Only supported by the system portal. + */ + space?: string; + + /** + * The line number that should be focued. + * Only supported by the system portal. + */ + lineNumber?: number; + + /** + * The column number that should be focused. + * Only supported by the system portal. + */ + columnNumber?: number; /** * The portal that the bot should be focused in. @@ -674,10 +670,9 @@ declare interface OpenBarcodeScannerAction extends Action { cameraType: CameraType; } - /** * Defines a photo that was taken. - * + * * @dochash types/camera * @docname Photo */ @@ -700,7 +695,7 @@ export interface Photo { /** * Options for {@link os.openPhotoCamera}. - * + * * @dochash types/camera * @doctitle Camera Types * @docsidebar Camera @@ -715,14 +710,14 @@ export interface OpenPhotoCameraOptions { /** * The image format that should be used. - * + * * Defaults to "png". */ imageFormat?: 'png' | 'jpeg'; /** * A number between 0 and 1 indicating the image quality to be used. - * + * * If not specified, then the browser will use its own default. */ imageQuality?: number; @@ -745,7 +740,7 @@ export interface OpenPhotoCameraOptions { /** * The ideal resolution for the photo to be taken at. - * + * * If specified, then the web browser will be told to prefer this resolution, but will use a lower resolution if * it is not possible to use the ideal resolution. */ @@ -759,11 +754,11 @@ export interface OpenPhotoCameraOptions { * The height of the photo in pixels. */ height: number; - } + }; /** * Whether to mirror the photo after it is taken. - * + * * Defaults to false. */ mirrorPhoto?: boolean; @@ -877,20 +872,20 @@ export interface ClassifyImagesAction extends AsyncAction { modelUrl?: string; /** - * The URL that the model JSON should be loaded from. - * Not required. Can be used if you are storing the model JSON in a custom location. - */ + * The URL that the model JSON should be loaded from. + * Not required. Can be used if you are storing the model JSON in a custom location. + */ modelJsonUrl?: string; /** - * The URL that the model metadata should be loaded from. - * Not required. Can be used if you are storing the model metadata in a custom location. - */ + * The URL that the model metadata should be loaded from. + * Not required. Can be used if you are storing the model metadata in a custom location. + */ modelMetadataUrl?: string; /** - * The images that should be classified. - */ + * The images that should be classified. + */ images: Image[]; } @@ -995,7 +990,7 @@ export type ImageClassifierOptions = Pick< export type ClassifyImagesOptions = Pick< ClassifyImagesAction, 'modelUrl' | 'modelJsonUrl' | 'modelMetadataUrl' | 'images' -> +>; /** * An event that is used to load a simulation. @@ -1148,7 +1143,7 @@ declare interface AnimateTagFunctionOptions { /** * The type of easing to use. * If not specified then "linear" "inout" will be used. - * + * * Can also be a custom function that takes a single parameter and returns a number. * The paramater will be a number between 0 and 1 indicating the progress through the tween. */ @@ -1372,7 +1367,14 @@ declare type ShowInputType = 'text' | 'color' | 'secret' | 'date' | 'list'; /** * Defines the possible input types. */ -declare type ShowInputSubtype = 'basic' | 'swatch' | 'advanced' | 'select' | 'multiSelect' | 'radio' | 'checkbox'; +declare type ShowInputSubtype = + | 'basic' + | 'swatch' + | 'advanced' + | 'select' + | 'multiSelect' + | 'radio' + | 'checkbox'; /** * Defines an event that is used to show a confirmation dialog. @@ -1531,8 +1533,8 @@ declare interface ShowChatOptions { backgroundColor?: string; /** - * The color to use for the foreground (text). - */ + * The color to use for the foreground (text). + */ foregroundColor?: string; } @@ -1648,7 +1650,16 @@ declare interface LocalFormAnimationAction { declare type TweenType = 'position' | 'rotation'; -declare type EaseType = 'linear' | 'quadratic' | 'cubic' | 'quartic' | 'quintic' | 'sinusoidal' | 'exponential' | 'circular' | 'elastic'; +declare type EaseType = + | 'linear' + | 'quadratic' + | 'cubic' + | 'quartic' + | 'quintic' + | 'sinusoidal' + | 'exponential' + | 'circular' + | 'elastic'; declare type EaseMode = 'in' | 'out' | 'inout'; @@ -1708,7 +1719,7 @@ declare interface LocalPositionTweenAction extends LocalTweenAction { /** * The target position of the tween. */ - position: { x?: number, y?: number, z?: number }; + position: { x?: number; y?: number; z?: number }; } /** @@ -1720,7 +1731,7 @@ declare interface LocalRotationTweenAction extends LocalTweenAction { /** * The target rotation of the tween. */ - rotation: { x?: number, y?: number, z?: number }; + rotation: { x?: number; y?: number; z?: number }; } /** @@ -1789,7 +1800,7 @@ declare interface EnablePOVAction { /** * The point that the camera should be placed at for POV. */ - center?: { x: number, y: number, z: number }; + center?: { x: number; y: number; z: number }; } /** @@ -1806,7 +1817,7 @@ declare interface WakeLockConfiguration { * An event that is used to send a command to the Jitsi Meet API. */ declare interface MeetCommandAction extends AsyncAction { - type: 'meet_command', + type: 'meet_command'; /** * The name of the command to execute. @@ -1919,7 +1930,7 @@ export interface AddDropSnapAction extends Action { * The ID of the bot that, when it is a drop target, the snap points should be enabled. * If null, then the targets apply globally during the drag operation. */ - botId?: string; + botId?: string; } /** @@ -1978,8 +1989,13 @@ export interface SnapAxis { * - "face" means that the dragged bot should snap to other bot faces. * - "bots" means that the dragged bot will snap to other bots. */ -declare type SnapTarget = 'ground' | 'grid' | 'face' | 'bots' | SnapPoint | SnapAxis; - +declare type SnapTarget = + | 'ground' + | 'grid' + | 'face' + | 'bots' + | SnapPoint + | SnapAxis; /** * An event that is used to add grids as possible drop locations for a drag operation. @@ -2002,13 +2018,13 @@ export interface SnapGrid { * The 3D position of the grid. * If not specified, then 0,0,0 is used. */ - position?: { x: number, y: number, z: number }; + position?: { x: number; y: number; z: number }; /** * The 3D rotation of the grid. * If not specified, then the identity rotation is used. */ - rotation?: { x: number, y: number, z: number, w?: number }; + rotation?: { x: number; y: number; z: number; w?: number }; /** * The ID of the bot that defines the portal that this grid should use. @@ -2033,7 +2049,7 @@ export interface SnapGrid { * The bounds that the snap grid has. * If not specified, then default bounds are used. */ - bounds?: { x: number, y: number }; + bounds?: { x: number; y: number }; /** * Whether to visualize the grid when dragging bots around. @@ -2044,7 +2060,7 @@ export interface SnapGrid { /** * The type of grid that this snap grid should be. * Defaults to the type of grid that the portal bot uses. - * + * * - "grid" indicates that the snap target should be a flat grid. * - "sphere" indicates that the snap target should be a sphere. */ @@ -2055,12 +2071,12 @@ export interface SnapGridTarget { /** * The 3D position that the grid should appear at. */ - position?: { x: number, y: number, z: number }; + position?: { x: number; y: number; z: number }; /** * The 3D rotation that the grid should appear at. */ - rotation?: { x: number, y: number, z: number, w?: number }; + rotation?: { x: number; y: number; z: number; w?: number }; /** * The bot that defines the portal that the grid should exist in. @@ -2077,7 +2093,7 @@ export interface SnapGridTarget { * The bounds of the grid. * Defaults to 10 x 10. */ - bounds?: { x: number, y: number }; + bounds?: { x: number; y: number }; /** * The priority that this grid should be evaluated in over other grids. @@ -2094,7 +2110,7 @@ export interface SnapGridTarget { /** * The type of grid that this snap grid should be. * Defaults to the type of grid that the portal bot uses. - * + * * - "grid" indicates that the snap target should be a flat grid. * - "sphere" indicates that the snap target should be a sphere. */ @@ -2135,7 +2151,6 @@ export interface RegisterCustomAppAction extends AsyncAction { * The options for a register custom portal action. */ export interface RegisterCustomAppOptions { - /** * The kind of the custom portal. * Used to make it easy to register multiple custom portals that rely on the same kind of renderers. @@ -2143,7 +2158,6 @@ export interface RegisterCustomAppOptions { kind?: string; } - /** * Defines an event that notifies that the output of a portal should be updated with the given data. */ @@ -2192,10 +2206,9 @@ declare interface RegisterPrefixOptions { /** * The name of the prefix. */ - name?: string; + name?: string; } - /** * An interface that represents the options that can be used for making recordings. */ @@ -2235,7 +2248,6 @@ declare interface EndRecordingAction extends AsyncAction { * Defines an interface that contains recorded data. */ declare interface Recording { - /** * The list of files that were produced when recording. */ @@ -2264,7 +2276,6 @@ declare interface RecordedFile { data: Blob; } - declare interface SpeakTextOptions { /** * The pitch that the text should be spoken at. @@ -2321,7 +2332,6 @@ declare interface SyntheticVoice { name: string; } - /** * An event that is used to retrieve the current geolocation of the device. */ @@ -2384,7 +2394,11 @@ declare interface UnsuccessfulGeolocation { /** * The code of the error that occurred. */ - errorCode?: 'permission_denied' | 'position_unavailable' | 'timeout' | 'unknown'; + errorCode?: + | 'permission_denied' + | 'position_unavailable' + | 'timeout' + | 'unknown'; /** * The message of the error that occurred. @@ -2681,7 +2695,7 @@ export type BotSpace = /** * The possible spaces that records can be stored in. - * + * * - "tempGlobal" means that the record is temporary and available to anyone. * - "tempRestricted" means that the record is temporary and available to a specific user. * - "permanentGlobal" means that the record is permanent and available to anyone. @@ -2757,8 +2771,8 @@ declare interface WebhookResult { * The HTTP headers that were returned with the response. */ headers: { - [key: string]: string - } + [key: string]: string; + }; } /** @@ -2842,7 +2856,7 @@ declare interface PerformanceStats { */ loadTimes: { [key: string]: number; - } + }; } /** @@ -2880,9 +2894,9 @@ export interface AttachDebuggerOptions { tagNameMapper?: TagMapper; } -/** +/** * Defines an interface that contains options for a debugger. -*/ + */ export interface CommonDebuggerOptions { /** * Whether to use "real" UUIDs instead of predictable ones. @@ -2897,9 +2911,9 @@ export interface CommonDebuggerOptions { allowAsynchronousScripts: boolean; /** - * The data that the configBot should be created from. - * Can be a mod or another bot. - */ + * The data that the configBot should be created from. + * Can be a mod or another bot. + */ configBot: Bot | BotTags; } @@ -2950,7 +2964,7 @@ export interface CreatePublicRecordKeyFailure { /** * The type of error that occurred. */ - errorCode: + errorCode: | UnauthorizedToCreateRecordKeyError | NotLoggedInError | 'unacceptable_session_key' @@ -2968,13 +2982,13 @@ export interface CreatePublicRecordKeyFailure { /** * The unique reason as to why the error occurred. */ - errorReason: - | 'user_denied' - | NotLoggedInError - | 'record_owned_by_different_user' - | 'invalid_policy' - | 'not_supported' - | ServerError; + errorReason: + | 'user_denied' + | NotLoggedInError + | 'record_owned_by_different_user' + | 'invalid_policy' + | 'not_supported' + | ServerError; } /** @@ -3011,14 +3025,14 @@ export type RecordNotFoundError = 'record_not_found'; */ export type NotAuthorizedError = 'not_authorized'; - -export type AuthorizeDeniedError = ServerError -| CreatePublicRecordKeyFailure['errorCode'] -| 'action_not_supported' -| 'not_logged_in' -| NotAuthorizedError -| 'unacceptable_request' -| 'subscription_limit_reached'; +export type AuthorizeDeniedError = + | ServerError + | CreatePublicRecordKeyFailure['errorCode'] + | 'action_not_supported' + | 'not_logged_in' + | NotAuthorizedError + | 'unacceptable_request' + | 'subscription_limit_reached'; export type UpdateUserPolicyError = 'policy_too_large'; @@ -3026,7 +3040,7 @@ export type GetPolicyError = 'policy_not_found'; /** * Defines a type that represents a policy that indicates which users are allowed to affect a record. - * + * * True indicates that any user can edit the record. * An array of strings indicates the list of users that are allowed to edit the record. */ @@ -3116,7 +3130,6 @@ export interface Permission { expireTimeMs: number | null; } - /** * Defines an interface that describes the common options for all permissions that affect data records. * @@ -3157,7 +3170,6 @@ export interface FilePermissionOptions { allowedMimeTypes?: true | string[]; } - /** * Defines an interface that describes the common options for all permissions that affect file records. * @@ -3182,7 +3194,6 @@ export interface FilePermission extends Permission { options: FilePermissionOptions; } - /** * Defines an interface that describes the common options for all permissions that affect event records. * @@ -3202,7 +3213,6 @@ export interface EventPermission extends Permission { action: EventActionKinds | null; } - /** * Defines an interface that describes the common options for all permissions that affect markers. * @@ -3222,7 +3232,6 @@ export interface MarkerPermission extends Permission { action: MarkerActionKinds | null; } - /** * Options for role permissions. * @@ -3237,7 +3246,6 @@ export interface RolePermissionOptions { maxDurationMs?: number; } - /** * Defines an interface that describes the common options for all permissions that affect roles. * @@ -3268,7 +3276,6 @@ export interface RolePermission extends Permission { options: RolePermissionOptions; } - /** * Defines an interface that describes common options for all permissions that affect insts. * @@ -3354,7 +3361,6 @@ export type InstActionKinds = | 'list' | 'sendAction'; - /** * Defines the possible results of granting a permission to a marker. * @@ -3396,9 +3402,7 @@ export interface GrantMarkerPermissionFailure { /** * The error code that indicates why the request failed. */ - errorCode: - | ServerError - | AuthorizeDeniedError; + errorCode: ServerError | AuthorizeDeniedError; /** * The error message that indicates why the request failed. @@ -3406,7 +3410,6 @@ export interface GrantMarkerPermissionFailure { errorMessage: string; } - /** * Defines the possible results of granting a permission to a resource. * @@ -3443,9 +3446,7 @@ export interface GrantResourcePermissionFailure { /** * The error code that indicates why the request failed. */ - errorCode: - | ServerError - | AuthorizeDeniedError; + errorCode: ServerError | AuthorizeDeniedError; /** * The error message that indicates why the request failed. @@ -3491,10 +3492,7 @@ export interface RevokePermissionFailure { /** * The error code that indicates why the request failed. */ - errorCode: - | ServerError - | 'permission_not_found' - | AuthorizeDeniedError; + errorCode: ServerError | 'permission_not_found' | AuthorizeDeniedError; /** * The error message that indicates why the request failed. @@ -3538,9 +3536,7 @@ export interface RevokeRoleFailure { /** * The error code that indicates why the request failed. */ - errorCode: - | ServerError - | AuthorizeDeniedError; + errorCode: ServerError | AuthorizeDeniedError; /** * The error message that indicates why the request failed. @@ -3556,17 +3552,14 @@ export interface GrantRoleSuccess { export interface GrantRoleFailure { success: false; - errorCode: - | ServerError - | AuthorizeDeniedError - | 'roles_too_large'; + errorCode: ServerError | AuthorizeDeniedError | 'roles_too_large'; errorMessage: string; } /** * The options for data record actions. */ - export interface RecordDataOptions { +export interface RecordDataOptions { /** * The HTTP Endpoint that should be queried. */ @@ -3600,7 +3593,6 @@ export interface GrantRoleFailure { * @docName ListDataOptions */ export interface ListDataOptions extends RecordActionOptions { - /** * The order that items should be sorted in. * - "ascending" means that the items should be sorted in alphebatically ascending order by address. @@ -3620,13 +3612,13 @@ export interface RecordDataSuccess { export interface RecordDataFailure { success: false; errorCode: - | ServerError - | NotLoggedInError - | InvalidRecordKey - | RecordNotFoundError - | NotSupportedError - | 'not_authorized' - | 'data_too_large'; + | ServerError + | NotLoggedInError + | InvalidRecordKey + | RecordNotFoundError + | NotSupportedError + | 'not_authorized' + | 'data_too_large'; errorMessage: string; } @@ -3661,11 +3653,14 @@ export interface GetDataSuccess { export interface GetDataFailure { success: false; - errorCode: ServerError | 'data_not_found' | 'not_authorized' | NotSupportedError; + errorCode: + | ServerError + | 'data_not_found' + | 'not_authorized' + | NotSupportedError; errorMessage: string; } - export type ListDataResult = ListDataSuccess | ListDataFailure; export interface ListDataSuccess { @@ -3679,13 +3674,10 @@ export interface ListDataSuccess { export interface ListDataFailure { success: false; - errorCode: - | ServerError - | NotSupportedError; + errorCode: ServerError | NotSupportedError; errorMessage: string; } - export type EraseDataResult = EraseDataSuccess | EraseDataFailure; export interface EraseDataSuccess { @@ -3696,17 +3688,17 @@ export interface EraseDataSuccess { export interface EraseDataFailure { success: false; - errorCode: ServerError - | NotLoggedInError - | InvalidRecordKey - | RecordNotFoundError - | NotSupportedError - | 'not_authorized' - | 'data_not_found'; + errorCode: + | ServerError + | NotLoggedInError + | InvalidRecordKey + | RecordNotFoundError + | NotSupportedError + | 'not_authorized' + | 'data_not_found'; errorMessage: string; } - export type RecordFileResult = RecordFileSuccess | RecordFileFailure; export interface RecordFileSuccess { @@ -3727,17 +3719,16 @@ export interface RecordFileSuccess { export interface RecordFileFailure { success: false; errorCode: - | ServerError - | NotLoggedInError - | InvalidRecordKey - | RecordNotFoundError - | 'file_already_exists' - | NotSupportedError - | 'invalid_file_data'; + | ServerError + | NotLoggedInError + | InvalidRecordKey + | RecordNotFoundError + | 'file_already_exists' + | NotSupportedError + | 'invalid_file_data'; errorMessage: string; } - export type EraseFileResult = EraseFileSuccess | EraseFileFailure; export interface EraseFileSuccess { success: true; @@ -3747,16 +3738,16 @@ export interface EraseFileSuccess { export interface EraseFileFailure { success: false; - errorCode: ServerError - | InvalidRecordKey - | RecordNotFoundError - | NotLoggedInError - | NotSupportedError - | 'file_not_found'; + errorCode: + | ServerError + | InvalidRecordKey + | RecordNotFoundError + | NotLoggedInError + | NotSupportedError + | 'file_not_found'; errorMessage: string; } - export type AddCountResult = AddCountSuccess | AddCountFailure; export interface AddCountSuccess { @@ -3769,11 +3760,11 @@ export interface AddCountSuccess { export interface AddCountFailure { success: false; errorCode: - | ServerError - | NotLoggedInError - | InvalidRecordKey - | RecordNotFoundError - | NotSupportedError + | ServerError + | NotLoggedInError + | InvalidRecordKey + | RecordNotFoundError + | NotSupportedError; errorMessage: string; } @@ -3803,9 +3794,7 @@ export interface GetCountSuccess { export interface GetCountFailure { success: false; - errorCode: - | ServerError - | NotSupportedError; + errorCode: ServerError | NotSupportedError; errorMessage: string; } @@ -3822,7 +3811,7 @@ export type ListStudiosResult = ListStudiosSuccess | ListStudiosFailure; /** * Defines an interface that represents a successful "list studios" result. - * + * * @dochash types/records/studios * @docname ListStudiosSuccess */ @@ -3837,13 +3826,13 @@ export interface ListStudiosSuccess { /** * Defines an interface that represents a failed "list studios" result. - * + * * @dochash types/records/studios * @docname ListStudiosFailure */ export interface ListStudiosFailure { success: false; - + /** * The error code. */ @@ -3857,7 +3846,7 @@ export interface ListStudiosFailure { /** * Defines an interface that represents a studio that a user has access to. - * + * * @dochash types/records/studios * @docname ListedStudio */ @@ -3896,7 +3885,6 @@ export interface ListedStudio { */ export type StudioAssignmentRole = 'admin' | 'member'; - export type JoinRoomResult = JoinRoomSuccess | JoinRoomFailure; export interface JoinRoomSuccess { @@ -3925,7 +3913,9 @@ export interface LeaveRoomFailure { errorMessage: string; } -export type SetRoomOptionsResult = SetRoomOptionsSuccess | SetRoomOptionsFailure; +export type SetRoomOptionsResult = + | SetRoomOptionsSuccess + | SetRoomOptionsFailure; export interface SetRoomOptionsSuccess { success: true; @@ -3939,7 +3929,9 @@ export interface SetRoomOptionsFailure { errorMessage: string; } -export type GetRoomOptionsResult = GetRoomOptionsSuccess | GetRoomOptionsFailure; +export type GetRoomOptionsResult = + | GetRoomOptionsSuccess + | GetRoomOptionsFailure; export interface GetRoomOptionsSuccess { success: true; @@ -3961,21 +3953,20 @@ export interface RoomOptions { * Whether to stream video. * Defaults to true. */ - video?: boolean; + video?: boolean; - /** - * Whether to stream audio. - * Defaults to true. - */ - audio?: boolean; - - /** - * Whether to stream the screen. - * Defaults to false. - */ - screen?: boolean; -} + /** + * Whether to stream audio. + * Defaults to true. + */ + audio?: boolean; + /** + * Whether to stream the screen. + * Defaults to false. + */ + screen?: boolean; +} export type GetRoomTrackOptionsResult = | GetRoomTrackOptionsSuccess @@ -4062,7 +4053,7 @@ export interface RoomTrackOptions { /** * The dimensions of the video if the track represents a video. */ - dimensions?: { width: number, height: number }; + dimensions?: { width: number; height: number }; /** * The aspect ratio of the video if the track represents a video. @@ -4078,7 +4069,11 @@ export type TrackKind = 'video' | 'audio'; /** * The possible sources for a room track. */ -export type TrackSource = 'camera' | 'microphone' | 'screen_share' | 'screen_share_audio'; +export type TrackSource = + | 'camera' + | 'microphone' + | 'screen_share' + | 'screen_share_audio'; /** * The possible video qualities for a room track. @@ -4090,37 +4085,37 @@ export interface JoinRoomActionOptions extends Partial { * The HTTP endpoint of the records website that should host the meeting. * If omitted, then the preconfigured records endpoint will be used. */ - endpoint?: string; + endpoint?: string; /** * The defaults that should be used for recording audio. * Should be an object. * See https://docs.livekit.io/client-sdk-js/interfaces/AudioCaptureOptions.html for a full list of properties. */ - audioCaptureDefaults: object; - - /** - * The defaults that should be used for recording video. Should be an object. - * See https://docs.livekit.io/client-sdk-js/interfaces/VideoCaptureOptions.html for a full list of properties. - */ - videoCaptureDefaults: object; - - /** - * The defaults that should be used for uploading audio/video content. - * See https://docs.livekit.io/client-sdk-js/interfaces/TrackPublishDefaults.html for a full list of properties. - */ - publishDefaults: object; - - /** - * Whether to enable dynacast. - * See https://docs.livekit.io/client-sdk-js/interfaces/RoomOptions.html#dynacast for more info. - */ - dynacast: boolean; - - /** - * Whether to enable adaptive streaming. Alternatively accepts an object with properties from this page: https://docs.livekit.io/client-sdk-js/modules.html#AdaptiveStreamSettings - */ - adaptiveStream: boolean | object; + audioCaptureDefaults: object; + + /** + * The defaults that should be used for recording video. Should be an object. + * See https://docs.livekit.io/client-sdk-js/interfaces/VideoCaptureOptions.html for a full list of properties. + */ + videoCaptureDefaults: object; + + /** + * The defaults that should be used for uploading audio/video content. + * See https://docs.livekit.io/client-sdk-js/interfaces/TrackPublishDefaults.html for a full list of properties. + */ + publishDefaults: object; + + /** + * Whether to enable dynacast. + * See https://docs.livekit.io/client-sdk-js/interfaces/RoomOptions.html#dynacast for more info. + */ + dynacast: boolean; + + /** + * Whether to enable adaptive streaming. Alternatively accepts an object with properties from this page: https://docs.livekit.io/client-sdk-js/modules.html#AdaptiveStreamSettings + */ + adaptiveStream: boolean | object; } export interface LeaveRoomActionOptions { @@ -4128,7 +4123,7 @@ export interface LeaveRoomActionOptions { * The HTTP endpoint of the records website that should host the meeting. * If omitted, then the preconfigured records endpoint will be used. */ - endpoint?: string; + endpoint?: string; } export type GetRoomRemoteOptionsResult = @@ -4184,7 +4179,7 @@ export interface RoomRemoteOptions { /** * Defines an interface that represents an update that has been applied to an inst. */ - export interface InstUpdate { +export interface InstUpdate { /** * The ID of the update. */ @@ -4204,7 +4199,7 @@ export interface RoomRemoteOptions { export interface AudioRecordingOptions { /** * Whether to stream the audio recording. - * If streaming is enabled, then @onAudioChunk will be triggered whenever a new + * If streaming is enabled, then @onAudioChunk will be triggered whenever a new * piece of audio is available. */ stream?: boolean; @@ -4252,7 +4247,6 @@ export interface PseudoRandomNumberGenerator { randomInt(min: number, max: number): number; } - // Luxon Types Copyright // MIT License @@ -4429,7 +4423,7 @@ declare class FixedOffsetZone extends Zone { /** * A zone that failed to parse. You should never need to instantiate this. */ -declare class InvalidZone extends Zone { } +declare class InvalidZone extends Zone {} /** * Represents the system zone for this JavaScript environment. @@ -4733,7 +4727,7 @@ declare class Interval { dateFormat: string, opts?: { separator?: string | undefined; - }, + } ): string; /** @@ -4754,7 +4748,10 @@ declare class Interval { * @example * Interval.fromDateTimes(dt1, dt2).toDuration('seconds').toObject() //=> { seconds: 88489.257 } */ - toDuration(unit?: DurationUnit | DurationUnit[], opts?: DiffOptions): Duration; + toDuration( + unit?: DurationUnit | DurationUnit[], + opts?: DiffOptions + ): Duration; /** * Run mapFn on the interval start and end, returning a new Interval from the resulting DateTimes @@ -4868,7 +4865,10 @@ declare namespace Info { * @param opts.locObj - an existing locale object to use. Defaults to null. * @param opts.outputCalendar - the calendar. Defaults to 'gregory'. */ - function monthsFormat(length?: UnitLength, options?: InfoCalendarOptions): string[]; + function monthsFormat( + length?: UnitLength, + options?: InfoCalendarOptions + ): string[]; /** * Return an array of standalone week names. @@ -4889,7 +4889,10 @@ declare namespace Info { * @example * Info.weekdays('short', { locale: 'ar' })[0] //=> 'الاثنين' */ - function weekdays(length?: StringUnitLength, options?: InfoUnitOptions): string[]; + function weekdays( + length?: StringUnitLength, + options?: InfoUnitOptions + ): string[]; /** * Return an array of format week names. @@ -4903,7 +4906,10 @@ declare namespace Info { * @param opts.numberingSystem - the numbering system. Defaults to null. * @param opts.locObj - an existing locale object to use. Defaults to null. */ - function weekdaysFormat(length?: StringUnitLength, options?: InfoUnitOptions): string[]; + function weekdaysFormat( + length?: StringUnitLength, + options?: InfoUnitOptions + ): string[]; /** * Return an array of meridiems. @@ -5068,7 +5074,10 @@ declare class Duration { * @param opts.numberingSystem - the numbering system to use * @param opts.conversionAccuracy - the conversion system to use. Defaults to 'casual'. */ - static fromObject(obj: DurationLikeObject, opts?: DurationOptions): Duration; + static fromObject( + obj: DurationLikeObject, + opts?: DurationOptions + ): Duration; /** * Create a Duration from DurationLike. @@ -5421,7 +5430,6 @@ declare class Duration { equals(other: Duration): boolean; } - export type DateTimeFormatOptions = Intl.DateTimeFormatOptions; export interface ZoneOptions { @@ -5440,49 +5448,53 @@ export interface ZoneOptions { /** @deprecated */ export type EraLength = StringUnitLength; -export type NumberingSystem = Intl.DateTimeFormatOptions extends { numberingSystem?: infer T } +export type NumberingSystem = Intl.DateTimeFormatOptions extends { + numberingSystem?: infer T; +} ? T : - | 'arab' - | 'arabext' - | 'bali' - | 'beng' - | 'deva' - | 'fullwide' - | 'gujr' - | 'guru' - | 'hanidec' - | 'khmr' - | 'knda' - | 'laoo' - | 'latn' - | 'limb' - | 'mlym' - | 'mong' - | 'mymr' - | 'orya' - | 'tamldec' - | 'telu' - | 'thai' - | 'tibt'; - -export type CalendarSystem = Intl.DateTimeFormatOptions extends { calendar?: infer T } + | 'arab' + | 'arabext' + | 'bali' + | 'beng' + | 'deva' + | 'fullwide' + | 'gujr' + | 'guru' + | 'hanidec' + | 'khmr' + | 'knda' + | 'laoo' + | 'latn' + | 'limb' + | 'mlym' + | 'mong' + | 'mymr' + | 'orya' + | 'tamldec' + | 'telu' + | 'thai' + | 'tibt'; + +export type CalendarSystem = Intl.DateTimeFormatOptions extends { + calendar?: infer T; +} ? T : - | 'buddhist' - | 'chinese' - | 'coptic' - | 'ethioaa' - | 'ethiopic' - | 'gregory' - | 'hebrew' - | 'indian' - | 'islamic' - | 'islamicc' - | 'iso8601' - | 'japanese' - | 'persian' - | 'roc'; + | 'buddhist' + | 'chinese' + | 'coptic' + | 'ethioaa' + | 'ethiopic' + | 'gregory' + | 'hebrew' + | 'indian' + | 'islamic' + | 'islamicc' + | 'iso8601' + | 'japanese' + | 'persian' + | 'roc'; export type HourCycle = 'h11' | 'h12' | 'h23' | 'h24'; @@ -5490,9 +5502,25 @@ export type StringUnitLength = 'narrow' | 'short' | 'long'; export type NumberUnitLength = 'numeric' | '2-digit'; export type UnitLength = StringUnitLength | NumberUnitLength; - -export type DateTimeUnit = 'year' | 'quarter' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond'; -export type ToRelativeUnit = 'years' | 'quarters' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'; +export type DateTimeUnit = + | 'year' + | 'quarter' + | 'month' + | 'week' + | 'day' + | 'hour' + | 'minute' + | 'second' + | 'millisecond'; +export type ToRelativeUnit = + | 'years' + | 'quarters' + | 'months' + | 'weeks' + | 'days' + | 'hours' + | 'minutes' + | 'seconds'; export type MonthNumbers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; export type WeekdayNumbers = 1 | 2 | 3 | 4 | 5 | 6 | 7; @@ -5691,7 +5719,8 @@ export interface ToObjectOutput extends DateTimeJSOptions { millisecond: number; } -export interface ToRelativeOptions extends Omit { +export interface ToRelativeOptions + extends Omit { /** * @default long */ @@ -5916,7 +5945,7 @@ declare class DateTime { minute: number, second: number, millisecond: number, - opts?: DateTimeJSOptions, + opts?: DateTimeJSOptions ): DateTime; static local( year: number, @@ -5925,7 +5954,7 @@ declare class DateTime { hour: number, minute: number, second: number, - opts?: DateTimeJSOptions, + opts?: DateTimeJSOptions ): DateTime; static local( year: number, @@ -5933,11 +5962,26 @@ declare class DateTime { day: number, hour: number, minute: number, - opts?: DateTimeJSOptions, + opts?: DateTimeJSOptions + ): DateTime; + static local( + year: number, + month: number, + day: number, + hour: number, + opts?: DateTimeJSOptions + ): DateTime; + static local( + year: number, + month: number, + day: number, + opts?: DateTimeJSOptions + ): DateTime; + static local( + year: number, + month: number, + opts?: DateTimeJSOptions ): DateTime; - static local(year: number, month: number, day: number, hour: number, opts?: DateTimeJSOptions): DateTime; - static local(year: number, month: number, day: number, opts?: DateTimeJSOptions): DateTime; - static local(year: number, month: number, opts?: DateTimeJSOptions): DateTime; static local(year: number, opts?: DateTimeJSOptions): DateTime; static local(opts?: DateTimeJSOptions): DateTime; @@ -5983,7 +6027,7 @@ declare class DateTime { minute: number, second: number, millisecond: number, - options?: LocaleOptions, + options?: LocaleOptions ): DateTime; static utc( year: number, @@ -5992,7 +6036,7 @@ declare class DateTime { hour: number, minute: number, second: number, - options?: LocaleOptions, + options?: LocaleOptions ): DateTime; static utc( year: number, @@ -6000,10 +6044,21 @@ declare class DateTime { day: number, hour: number, minute: number, - options?: LocaleOptions, + options?: LocaleOptions + ): DateTime; + static utc( + year: number, + month: number, + day: number, + hour: number, + options?: LocaleOptions + ): DateTime; + static utc( + year: number, + month: number, + day: number, + options?: LocaleOptions ): DateTime; - static utc(year: number, month: number, day: number, hour: number, options?: LocaleOptions): DateTime; - static utc(year: number, month: number, day: number, options?: LocaleOptions): DateTime; static utc(year: number, month: number, options?: LocaleOptions): DateTime; static utc(year: number, options?: LocaleOptions): DateTime; static utc(options?: LocaleOptions): DateTime; @@ -6027,7 +6082,10 @@ declare class DateTime { * @param options.outputCalendar - the output calendar to set on the resulting DateTime instance * @param options.numberingSystem - the numbering system to set on the resulting DateTime instance */ - static fromMillis(milliseconds: number, options?: DateTimeJSOptions): DateTime; + static fromMillis( + milliseconds: number, + options?: DateTimeJSOptions + ): DateTime; /** * Create a DateTime from a number of seconds since the epoch (meaning since 1 January 1970 00:00:00 UTC). Uses the default zone. @@ -6162,12 +6220,20 @@ declare class DateTime { * @param opts.numberingSystem - the numbering system to use when parsing. Will also set the resulting DateTime to this numbering system * @param opts.outputCalendar - the output calendar to set on the resulting DateTime instance */ - static fromFormat(text: string, fmt: string, opts?: DateTimeOptions): DateTime; + static fromFormat( + text: string, + fmt: string, + opts?: DateTimeOptions + ): DateTime; /** * @deprecated use fromFormat instead */ - static fromString(text: string, format: string, options?: DateTimeOptions): DateTime; + static fromString( + text: string, + format: string, + options?: DateTimeOptions + ): DateTime; /** * Create a DateTime from a SQL date, time, or datetime @@ -6481,7 +6547,9 @@ declare class DateTime { * * @param opts - the same options as toLocaleString */ - resolvedLocaleOptions(opts?: LocaleOptions | DateTimeFormatOptions): ResolvedLocaleOptions; + resolvedLocaleOptions( + opts?: LocaleOptions | DateTimeFormatOptions + ): ResolvedLocaleOptions; // TRANSFORM @@ -6665,7 +6733,10 @@ declare class DateTime { * @example * DateTime.now().toLocaleString({ hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }); //=> '11:32' */ - toLocaleString(formatOpts?: DateTimeFormatOptions, opts?: LocaleOptions): string; + toLocaleString( + formatOpts?: DateTimeFormatOptions, + opts?: LocaleOptions + ): string; /** * Returns an array of format "parts", meaning individual tokens along with metadata. This is allows callers to post-process individual sections of the formatted output. @@ -6883,7 +6954,11 @@ declare class DateTime { * i2.diff(i1, ['months', 'days']).toObject() //=> { months: 16, days: 19.03125 } * i2.diff(i1, ['months', 'days', 'hours']).toObject() //=> { months: 16, days: 19, hours: 0.75 } */ - diff(otherDateTime: DateTime, unit?: DurationUnits, opts?: DiffOptions): Duration; + diff( + otherDateTime: DateTime, + unit?: DurationUnits, + opts?: DiffOptions + ): Duration; /** * Return the difference between this DateTime and right now. @@ -6997,12 +7072,20 @@ declare class DateTime { * @param fmt - the format the string is expected to be in (see description) * @param options - options taken by fromFormat() */ - static fromFormatExplain(text: string, fmt: string, options?: DateTimeOptions): ExplainedFormat; + static fromFormatExplain( + text: string, + fmt: string, + options?: DateTimeOptions + ): ExplainedFormat; /** * @deprecated use fromFormatExplain instead */ - static fromStringExplain(text: string, fmt: string, options?: DateTimeOptions): ExplainedFormat; + static fromStringExplain( + text: string, + fmt: string, + options?: DateTimeOptions + ): ExplainedFormat; // FORMAT PRESETS @@ -7120,7 +7203,6 @@ declare class DateTime { declare type DateTimeClass = typeof DateTime; // End Luxon Types - // Begin Math Types type Vector2Class = typeof Vector2; type Vector3Class = typeof Vector3; @@ -7169,7 +7251,11 @@ declare class Vector2 { * @param finish The end position. * @param amount The amount that the resulting position should be interpolated between the start and end positions. Values near 0 indicate rotations close to the first and values near 1 indicate rotations close to the second. */ - static interpolatePosition(start: Vector2, finish: Vector2, amount: number): Vector2; + static interpolatePosition( + start: Vector2, + finish: Vector2, + amount: number + ): Vector2; /** * Constructs a new vector that is the directional linear interpolation between the given start and end positions. * The degree that the result is interpolated is determined by the given amount parameter. @@ -7180,7 +7266,11 @@ declare class Vector2 { * @param finish The end position. * @param amount The amount that the resulting position should be interpolated between the start and end positions. Values near 0 indicate rotations close to the first and values near 1 indicate rotations close to the second. */ - static interpolateDirection(start: Vector2, finish: Vector2, amount: number): Vector2; + static interpolateDirection( + start: Vector2, + finish: Vector2, + amount: number + ): Vector2; /** * Adds this vector with the other vector and returns the result. * @param other The other vector to add with this vector. @@ -7282,7 +7372,11 @@ declare class Vector3 { * @param finish The end position. * @param amount The amount that the resulting position should be interpolated between the start and end positions. Values near 0 indicate rotations close to the first and values near 1 indicate rotations close to the second. */ - static interpolatePosition(start: Vector3, finish: Vector3, amount: number): Vector3; + static interpolatePosition( + start: Vector3, + finish: Vector3, + amount: number + ): Vector3; /** * Constructs a new vector that is the directional linear interpolation between the given start and end positions. * The degree that the result is interpolated is determined by the given amount parameter. @@ -7293,7 +7387,11 @@ declare class Vector3 { * @param finish The end position. * @param amount The amount that the resulting position should be interpolated between the start and end positions. Values near 0 indicate rotations close to the first and values near 1 indicate rotations close to the second. */ - static interpolateDirection(start: Vector3, finish: Vector3, amount: number): Vector3; + static interpolateDirection( + start: Vector3, + finish: Vector3, + amount: number + ): Vector3; /** * Adds this vector with the other vector and returns the result. * @param other The other vector to add with this vector. @@ -7452,7 +7550,16 @@ declare class Rotation { * Creates a new rotation using the given parameters. * @param rotation The information that should be used to construct the rotation. */ - constructor(rotation?: FromToRotation | AxisAndAngle | QuaternionRotation | Quaternion | SequenceRotation | EulerAnglesRotation | LookRotation); + constructor( + rotation?: + | FromToRotation + | AxisAndAngle + | QuaternionRotation + | Quaternion + | SequenceRotation + | EulerAnglesRotation + | LookRotation + ); /** * Constructs a new Quaternion from the given axis and angle. * @param axisAndAngle The object that contains the axis and angle values. @@ -7482,7 +7589,11 @@ declare class Rotation { * @param second The second rotation. * @param amount The amount that the resulting rotation should be interpolated between the first and second rotations. Values near 0 indicate rotations close to the first and values near 1 indicate rotations close to the second. */ - static interpolate(first: Rotation, second: Rotation, amount: number): Rotation; + static interpolate( + first: Rotation, + second: Rotation, + amount: number + ): Rotation; /** * Rotates the given Vector3 by this quaternion and returns a new vector containing the result. * @param vector The 3D vector that should be rotated. @@ -7574,13 +7685,13 @@ export interface SequenceRotation { sequence: Rotation[]; } export interface QuaternionRotation { - quaternion: { x: number, y: number, z: number, w: number }; + quaternion: { x: number; y: number; z: number; w: number }; } /** * Defines an interface that represents a rotation transforms (0, 1, 0) and (0, 0, 1) to look along the given direction and upwards axes. */ - export interface LookRotation { +export interface LookRotation { /** * The direction that (0, 1, 0) should be pointing along after the rotation is applied. */ @@ -7612,11 +7723,10 @@ export interface QuaternionRotation { // End Math Types - /** * Defines an interface that represents the result of a raycast operation. */ - export interface RaycastResult { +export interface RaycastResult { /** * The list of intersections. */ @@ -7667,9 +7777,9 @@ export interface BotIntersection { */ portal: string; - /** - * The dimension that the bot is in. - */ + /** + * The dimension that the bot is in. + */ dimension: string; } @@ -7922,9 +8032,22 @@ export interface PreactContext { export type RefObject = { current: T | null }; +interface XpUser { + /** The unique id of the model item */ + id: string; + /** The id of the associated account (null if not yet set up) */ + accountId: string | null; + /** The rate at which the user is requesting payment (null if not yet specified) */ + requestedRate: number | null; + /** The users unique id from the Auth system */ + userId: string; + /** The date at which the entity was created */ + createdAtMs: number; + /** The date at which the entity was last updated */ + updatedAtMs: number; +} declare global { - /** * The Bot that this script is running in. */ @@ -8086,12 +8209,16 @@ declare global { * @param bot The bot or list of bots that the tag should be renamed on. * @param originalTag The original tag to rename. * @param newTag The new tag name. - * + * * @example * // Rename the "abc" tag to "def" * renameTag(this, "abc", "def") */ - function renameTag(bot: Bot | Bot[], originalTag: string, newTag: string): void; + function renameTag( + bot: Bot | Bot[], + originalTag: string, + newTag: string + ): void; /** * Gets the ID from the given bot. @@ -8123,7 +8250,10 @@ declare global { * @param first The first snapshot. * @param second The second snapshot. */ - function diffSnapshots(first: BotsState, second: BotsState): PartialBotsState; + function diffSnapshots( + first: BotsState, + second: BotsState + ): PartialBotsState; /** * Applies the given delta to the given snapshot and returns the result. @@ -8131,7 +8261,10 @@ declare global { * @param snapshot The snapshot that the diff should be applied to. * @param diff The delta that should be applied to the snapshot. */ - function applyDiffToSnapshot(snapshot: BotsState, diff: PartialBotsState): BotsState; + function applyDiffToSnapshot( + snapshot: BotsState, + diff: PartialBotsState + ): BotsState; /** * Shouts the given events in order until a bot returns a result. @@ -8142,9 +8275,9 @@ declare global { function priorityShout(eventNames: string[], arg?: any): any; /** - * Creates a tag value that can be used to link to the given bots. - * @param bots The bots that the link should point to. - */ + * Creates a tag value that can be used to link to the given bots. + * @param bots The bots that the link should point to. + */ function getLink(...bots: (Bot | string | (Bot | string)[])[]): string; /** @@ -8159,7 +8292,10 @@ declare global { * @param bot The bot to update. * @param idMap The map of old IDs to new IDs that should be used. */ - function updateBotLinks(bot: Bot, idMap: Map | { [id: string]: string | Bot }): void; + function updateBotLinks( + bot: Bot, + idMap: Map | { [id: string]: string | Bot } + ): void; /** * Parses the given value into a date time object. @@ -8209,7 +8345,7 @@ declare global { * // Tell every bot say "Hi" to you. * shout("sayHi()", "My Name"); */ - (name: string, arg?: any): any[], + (name: string, arg?: any): any[]; [name: string]: { /** * Asks every bot in the inst to run the given action. @@ -8218,8 +8354,8 @@ declare global { * @param arg The optional argument to include in the shout. * @returns Returns a list which contains the values returned from each script that was run for the shout. */ - (arg?: any): any[] - } + (arg?: any): any[]; + }; }; /** @@ -8262,7 +8398,10 @@ declare global { * @param bot The bot or list of bots that should be watched for changes. * @param callback The function that should be called when the bot is changed or destroyed. */ - function watchBot(bot: (Bot | string)[] | Bot | string, callback: () => void): number; + function watchBot( + bot: (Bot | string)[] | Bot | string, + callback: () => void + ): number; /** * Cancels watching a bot using the given ID number that was returned from watchBot(). @@ -8318,8 +8457,8 @@ declare global { const web: Web; /** - * Creates a Universally Unique IDentifier (UUID). - */ + * Creates a Universally Unique IDentifier (UUID). + */ function uuid(): string; /** @@ -8328,14 +8467,21 @@ declare global { * @param tag The tag that should be animated. * @param options The options for the animation. If given null, then any running animations for the given tag will be canceled. */ - function animateTag(bot: Bot | (Bot | string)[] | string, tag: string, options: AnimateTagFunctionOptions): Promise; + function animateTag( + bot: Bot | (Bot | string)[] | string, + tag: string, + options: AnimateTagFunctionOptions + ): Promise; /** * Animates the given tags. Returns a promise when the animation is finished. * @param bot The bot or list of bots that should be animated. * @param options The options for the animation. fromValue should be an object which contains the starting tag values and toValue should be an object that contains the ending tag values. */ - function animateTag(bot: Bot | (Bot | string)[] | string, options: AnimateTagFunctionOptions): Promise; + function animateTag( + bot: Bot | (Bot | string)[] | string, + options: AnimateTagFunctionOptions + ): Promise; /** * Animates the given tag. Returns a promise when the animation is finished. * @param bot The bot or list of bots that should be animated. @@ -8353,7 +8499,10 @@ declare global { * @param bot The bot or list of bots that should cancel their animations. * @param tag The tag that the animations should be canceld for. If omitted then all tags will be canceled. */ - function clearAnimations(bot: Bot | (Bot | string)[] | string, tag?: string): void; + function clearAnimations( + bot: Bot | (Bot | string)[] | string, + tag?: string + ): void; /** * Sends the given operation to all the devices that matches the given selector. @@ -8381,14 +8530,18 @@ declare global { /** * Sends an event to the given remote or list of remotes. * The other remotes will recieve an onRemoteData shout for this whisper. - * + * * In effect, this allows remotes to communicate with each other by sending arbitrary events. - * + * * @param remoteId The ID of the other remote or remotes to whisper to. * @param name The name of the event. * @param arg The optional argument to include in the event. */ - function sendRemoteData(remoteId: string | string[], name: string, arg?: any): RemoteAction | RemoteAction[]; + function sendRemoteData( + remoteId: string | string[], + name: string, + arg?: any + ): RemoteAction | RemoteAction[]; /** * Gets the first bot which matches all of the given filters. @@ -8493,7 +8646,7 @@ declare global { /** * Creates a filter function that checks whether bots have the given ID. * @param id The ID to check for. - * + * * @example * // Find all the bots with the ID "bob". * let bobs = getBots(byId("bob")); @@ -8642,12 +8795,17 @@ declare global { * @param tag The tag to set. * @param value The value to set. * @param space The space that the tag mask should be placed in. If not specified, then the tempLocal space will be used. - * + * * @example * // Set a bot's color to "green". * setTagMask(this, "color", "green") */ - function setTagMask(bot: Bot | Bot[], tag: string, value: any, space?: BotSpace): any; + function setTagMask( + bot: Bot | Bot[], + tag: string, + value: any, + space?: BotSpace + ): any; /** * Clears the tag masks from the given bot. @@ -8733,20 +8891,14 @@ declare global { * @param bot The bot or bot ID. * @param dimension The dimension that the bot's position should be retrieved for. */ - function getBotPosition( - bot: Bot | string, - dimension: string - ): Vector3; + function getBotPosition(bot: Bot | string, dimension: string): Vector3; /** * Gets the rotation that the given bot is at in the given dimension. * @param bot The bot or bot ID. * @param dimension The dimension that the bot's rotation should be retrieved for. */ - function getBotRotation( - bot: Bot | string, - dimension: string - ): Rotation; + function getBotRotation(bot: Bot | string, dimension: string): Rotation; /** * Applies the given diff to the given bot. @@ -8778,6 +8930,11 @@ declare global { */ const ai: Ai; + /** + * Defines a set of functions that relate to Xp system operations. + */ + const xp: Xp; + /** * Defines a set of functions that relate to common server operations. * Typically, these operations are instance-independent. @@ -8829,7 +8986,6 @@ declare global { } interface DebuggerBase { - /** * Gets the config bot from the debugger. * May be null. @@ -8853,9 +9009,9 @@ interface DebuggerBase { getCommonActions(): BotAction[]; /** - * Gets the list of bot actions that have been performed by bots in this debugger. - * Bot actions are actions that directly create/destroy/update bots or bot tags. - */ + * Gets the list of bot actions that have been performed by bots in this debugger. + * Bot actions are actions that directly create/destroy/update bots or bot tags. + */ getBotActions(): BotAction[]; /** @@ -8893,13 +9049,17 @@ interface DebuggerBase { * Registers the given handler to be called after a bot script changes a tag value. * @param listener The listener that should be executed. */ - onAfterScriptUpdatedTag(listener: (update: DebuggerTagUpdate) => void): void; + onAfterScriptUpdatedTag( + listener: (update: DebuggerTagUpdate) => void + ): void; /** * Registers the given handler to be called after a bot script changes a tag mask value. * @param listener The listener that should be executed. */ - onAfterScriptUpdatedTagMask(listener: (update: DebuggerTagMaskUpdate) => void): void; + onAfterScriptUpdatedTagMask( + listener: (update: DebuggerTagMaskUpdate) => void + ): void; /** * Gets the current call stack for the interpreter. @@ -8929,7 +9089,11 @@ interface DebuggerBase { * @param tag The tag that the trigger should be registered in. Required if a bot or bot ID is specified. * @param options The options that go with this pause trigger. Required if a bot or bot ID is specified. */ - setPauseTrigger(botOrIdOrTrigger: Bot | string | PauseTrigger, tag?: string, options?: PauseTriggerOptions): PauseTrigger; + setPauseTrigger( + botOrIdOrTrigger: Bot | string | PauseTrigger, + tag?: string, + options?: PauseTriggerOptions + ): PauseTrigger; /** * Registers a new pause trigger with this debugger. * Pause triggers can be used to tell the debugger when you want it to stop execution. @@ -8938,7 +9102,11 @@ interface DebuggerBase { * @param tag The tag that the trigger should be placed in. * @param options The options that go with this pause trigger. */ - setPauseTrigger(botOrId: Bot | string, tag: string, options: PauseTriggerOptions): PauseTrigger; + setPauseTrigger( + botOrId: Bot | string, + tag: string, + options: PauseTriggerOptions + ): PauseTrigger; /** * Registers or updates the given pause trigger with this debugger. @@ -8975,7 +9143,10 @@ interface DebuggerBase { * @param botOrId The bot or bot ID. * @param tag The name of the tag that the trigger locations should be listed for. */ - listCommonPauseTriggers(botOrId: Bot | string, tag: string): PossiblePauseTriggerLocation[]; + listCommonPauseTriggers( + botOrId: Bot | string, + tag: string + ): PossiblePauseTriggerLocation[]; /** * Resumes the debugger execution from the given pause. @@ -9005,7 +9176,7 @@ interface DebuggerBase { * @param bot The bot or list of bots that the tag should be renamed on. * @param originalTag The original tag to rename. * @param newTag The new tag name. - * + * * @example * // Rename the "abc" tag to "def" * renameTag(this, "abc", "def") @@ -9025,10 +9196,10 @@ interface DebuggerBase { getJSON(data: any): string; /** - * Gets nicely formatted JSON for the given data. - * @param data The data. - */ - getFormattedJSON(data: any): string; + * Gets nicely formatted JSON for the given data. + * @param data The data. + */ + getFormattedJSON(data: any): string; /** * Makes a snapshot of the given bot(s). @@ -9065,10 +9236,10 @@ interface DebuggerBase { fromBase64String(base64: string): Uint8Array; /** - * Creates a tag value that can be used to link to the given bots. - * @param bots The bots that the link should point to. - */ - getLink(...bots: (Bot | string | (Bot | string)[])[]): string; + * Creates a tag value that can be used to link to the given bots. + * @param bots The bots that the link should point to. + */ + getLink(...bots: (Bot | string | (Bot | string)[])[]): string; /** * Gets the list of bot links that are stored in this bot's tags. @@ -9077,12 +9248,15 @@ interface DebuggerBase { getBotLinks(bot: Bot): ParsedBotLink[]; /** - * Updates all the links in the given bot using the given ID map. - * Useful if you know that the links in the given bot are outdated and you know which IDs map to the new IDs. - * @param bot The bot to update. - * @param idMap The map of old IDs to new IDs that should be used. - */ - updateBotLinks(bot: Bot, idMap: Map | { [id: string]: string | Bot }): void + * Updates all the links in the given bot using the given ID map. + * Useful if you know that the links in the given bot are outdated and you know which IDs map to the new IDs. + * @param bot The bot to update. + * @param idMap The map of old IDs to new IDs that should be used. + */ + updateBotLinks( + bot: Bot, + idMap: Map | { [id: string]: string | Bot } + ): void; /** * Shouts the given event to every bot in every loaded simulation. @@ -9097,7 +9271,10 @@ interface DebuggerBase { * @param bot The bot or list of bots that should be watched for changes. * @param callback The function that should be called when the bot is changed or destroyed. */ - watchBot(bot: (Bot | string)[] | Bot | string, callback: () => void): number; + watchBot( + bot: (Bot | string)[] | Bot | string, + callback: () => void + ): number; /** * Cancels watching a bot using the given ID number that was returned from watchBot(). @@ -9120,11 +9297,11 @@ interface DebuggerBase { clearWatchPortal(watchId: number): void; /** - * Asserts that the given condition is true. - * Throws an error if the condition is not true. - * @param condition The condition to check. - * @param message The message to use in the error if the condition is not true. - */ + * Asserts that the given condition is true. + * Throws an error if the condition is not true. + * @param condition The condition to check. + * @param message The message to use in the error if the condition is not true. + */ assert(condition: boolean, message?: string): void; /** @@ -9153,8 +9330,8 @@ interface DebuggerBase { web: Web; /** - * Creates a Universally Unique IDentifier (UUID). - */ + * Creates a Universally Unique IDentifier (UUID). + */ uuid(): string; /** @@ -9163,14 +9340,21 @@ interface DebuggerBase { * @param tag The tag that should be animated. * @param options The options for the animation. If given null, then any running animations for the given tag will be canceled. */ - animateTag(bot: Bot | (Bot | string)[] | string, tag: string, options: AnimateTagFunctionOptions): Promise; + animateTag( + bot: Bot | (Bot | string)[] | string, + tag: string, + options: AnimateTagFunctionOptions + ): Promise; /** * Animates the given tags. Returns a promise when the animation is finished. * @param bot The bot or list of bots that should be animated. * @param options The options for the animation. fromValue should be an object which contains the starting tag values and toValue should be an object that contains the ending tag values. */ - animateTag(bot: Bot | (Bot | string)[] | string, options: AnimateTagFunctionOptions): Promise; + animateTag( + bot: Bot | (Bot | string)[] | string, + options: AnimateTagFunctionOptions + ): Promise; /** * Animates the given tag. Returns a promise when the animation is finished. * @param bot The bot or list of bots that should be animated. @@ -9216,14 +9400,18 @@ interface DebuggerBase { /** * Sends an event to the given remote or list of remotes. * The other remotes will recieve an onRemoteData shout for this whisper. - * + * * In effect, this allows remotes to communicate with each other by sending arbitrary events. - * + * * @param remoteId The ID of the other remote or remotes to whisper to. * @param name The name of the event. * @param arg The optional argument to include in the event. */ - sendRemoteData(remoteId: string | string[], name: string, arg?: any): RemoteAction | RemoteAction[]; + sendRemoteData( + remoteId: string | string[], + name: string, + arg?: any + ): RemoteAction | RemoteAction[]; /** * Gets the first bot which matches all of the given filters. @@ -9328,7 +9516,7 @@ interface DebuggerBase { /** * Creates a filter function that checks whether bots have the given ID. * @param id The ID to check for. - * + * * @example * // Find all the bots with the ID "bob". * let bobs = getBots(byId("bob")); @@ -9370,11 +9558,7 @@ interface DebuggerBase { * // Find all the bots at (1, 2) in the "test" dimension. * let bots = getBots(atPosition("test", 1, 2)); */ - atPosition( - dimension: string, - x: number, - y: number - ): BotFilterFunction; + atPosition(dimension: string, x: number, y: number): BotFilterFunction; /** * Creates a filter function that checks whether bots were created by the given bot. @@ -9477,12 +9661,17 @@ interface DebuggerBase { * @param tag The tag to set. * @param value The value to set. * @param space The space that the tag mask should be placed in. If not specified, then the tempLocal space will be used. - * + * * @example * // Set a bot's color to "green". * setTagMask(this, "color", "green") */ - setTagMask(bot: Bot | Bot[], tag: string, value: any, space?: BotSpace): any; + setTagMask( + bot: Bot | Bot[], + tag: string, + value: any, + space?: BotSpace + ): any; /** * Clears the tag masks from the given bot. @@ -9499,12 +9688,7 @@ interface DebuggerBase { * @param index The index that the text should be inserted at. * @param text The text that should be inserted. */ - insertTagText( - bot: Bot, - tag: string, - index: number, - text: string - ): string; + insertTagText(bot: Bot, tag: string, index: number, text: string): string; /** * Inserts the given text into the given tag and space at the given index. @@ -9531,12 +9715,7 @@ interface DebuggerBase { * @param index The index that the text should be deleted at. * @param count The number of characters to delete. */ - deleteTagText( - bot: Bot, - tag: string, - index: number, - count: number - ): string; + deleteTagText(bot: Bot, tag: string, index: number, count: number): string; /** * Deletes the specified number of characters from the given tag mask. @@ -9568,20 +9747,14 @@ interface DebuggerBase { * @param bot The bot or bot ID. * @param dimension The dimension that the bot's position should be retrieved for. */ - getBotPosition( - bot: Bot | string, - dimension: string - ): Vector3; + getBotPosition(bot: Bot | string, dimension: string): Vector3; /** * Gets the rotation that the given bot is at in the given dimension. * @param bot The bot or bot ID. * @param dimension The dimension that the bot's rotation should be retrieved for. */ - getBotRotation( - bot: Bot | string, - dimension: string - ): Rotation; + getBotRotation(bot: Bot | string, dimension: string): Rotation; /** * Applies the given diff to the given bot. @@ -9633,7 +9806,7 @@ interface DebuggerBase { * Defines a set of functions that are used to create and transform mods. */ mod: ModFuncs; - + /** * Defines a set of functions that are used to transform byte arrays. */ @@ -9682,13 +9855,13 @@ interface Debugger extends DebuggerBase { */ create(...mods: Mod[]): Bot | Bot[]; - /** - * Destroys the given bot, bot ID, or list of bots. - * @param bot The bot, bot ID, or list of bots to destroy. - */ + /** + * Destroys the given bot, bot ID, or list of bots. + * @param bot The bot, bot ID, or list of bots to destroy. + */ destroy(bot: Bot | string | Bot[]): void; - /** + /** * Shouts the given events in order until a bot returns a result. * Returns the result that was produced or undefined if no result was produced. * @param eventNames The names of the events to shout. @@ -9778,13 +9951,13 @@ interface PausableDebugger extends DebuggerBase { */ create(...mods: Mod[]): Promise; - /** - * Destroys the given bot, bot ID, or list of bots. - * @param bot The bot, bot ID, or list of bots to destroy. - */ + /** + * Destroys the given bot, bot ID, or list of bots. + * @param bot The bot, bot ID, or list of bots to destroy. + */ destroy(bot: Bot | string | Bot[]): Promise; - /** + /** * Shouts the given events in order until a bot returns a result. * Returns the result that was produced or undefined if no result was produced. * @param eventNames The names of the events to shout. @@ -9860,13 +10033,12 @@ interface RecordFileOptions { description?: string; /** - * The MIME type of the file. + * The MIME type of the file. * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types for more information. */ mimeType?: string; } - /** * Defines an interface that contains a bunch of options for starting an animation. */ @@ -9952,7 +10124,7 @@ export interface StopFormAnimationOptions { /** * Defines an interface that contains animation information. */ - export interface FormAnimationData { +export interface FormAnimationData { /** * The name of the animation. */ @@ -9969,7 +10141,6 @@ export interface StopFormAnimationOptions { duration: number; } - /** * Defines an interface that represents a single chat message in a conversation with an AI. * @@ -10006,7 +10177,6 @@ export interface AIChatMessage { finishReason?: string; } - /** * Defines an interface that represents the contents of an AI chat message. * @@ -10063,7 +10233,6 @@ export interface AIUrlContent { url: string; } - /** * Defines an interface that represents options for {@link ai.chat}. * @@ -10111,7 +10280,7 @@ export interface AIChatOptions extends RecordActionOptions { /** * The list of stop words that should be used. - * + * * If the AI generates a sequence of tokens that match one of the given words, then it will stop generating tokens. */ stopWords?: string[]; @@ -10155,7 +10324,7 @@ export interface AIGenerateSkyboxBlockadeLabsOptions { /** * Defines an interface that represents a request for {@link ai.generateSkybox-request}. - * + * * @dochash types/ai * @docname AIGenerateSkyboxRequest */ @@ -10178,7 +10347,7 @@ export interface AIGenerateSkyboxRequest { /** * Defines an interface that represents the result from {@link ai.generateSkybox-request}. - * + * * @dochash types/ai * @docname AIGenerateSkyboxResult */ @@ -10194,10 +10363,9 @@ export interface AIGenerateSkyboxResult { thumbnailUrl?: string; } - /** * Defines an interface that represents options for {@link ai.generateImage-string}. - * + * * @dochash types/ai * @docname AIGenerateImageOptions */ @@ -10278,10 +10446,9 @@ export interface AIGenerateImageSuccess { images: AIGeneratedImage[]; } - /** * Defines an interface that represents an AI generated image. - * + * * @dochash types/ai * @docname AIGeneratedImage */ @@ -10302,7 +10469,6 @@ export interface AIGeneratedImage { seed?: number; } - export type AIHumeGetAccessTokenResult = | AIHumeGetAccessTokenSuccess | AIHumeGetAccessTokenFailure; @@ -10341,10 +10507,9 @@ export interface AIHumeGetAccessTokenFailure { errorMessage: string; } - /** * The response to a request to generate a model using the Sloyd AI interface. - * + * * @dochash types/ai * @docname AISloydGenerateModelResponse */ @@ -10354,7 +10519,7 @@ export type AISloydGenerateModelResponse = /** * A successful response to a request to generate a model using the Sloyd AI interface. - * + * * @dochash types/ai * @docname AISloydGenerateModelSuccess */ @@ -10396,7 +10561,7 @@ export interface AISloydGenerateModelSuccess { /** * A failed response to a request to generate a model using the Sloyd AI interface. - * + * * @dochash types/ai * @docname AISloydGenerateModelFailure */ @@ -10418,10 +10583,9 @@ export interface AISloydGenerateModelFailure { errorMessage: string; } - /** * The options for generating a model using Sloyd AI. - * + * * @dochash types/ai * @docname AISloydGenerateModelOptions */ @@ -10473,7 +10637,7 @@ export interface AISloydGenerateModelOptions { * The desired height of the thumbnail in pixels. */ height: number; - } + }; } /** @@ -10638,7 +10802,6 @@ export interface CrudEraseItemFailure { errorMessage: string; } - export type CrudListItemsResult = | CrudListItemsSuccess | CrudListItemsFailure; @@ -10677,7 +10840,6 @@ export interface CrudListItemsFailure { errorMessage: string; } - export type HandleWebhookResult = HandleWebhookSuccess | HandleWebhookFailure; export interface HandleWebhookSuccess { @@ -10710,7 +10872,6 @@ export interface HandleWebhookFailure { errorMessage: string; } - /** * Defines an interface that represents the options for a list data action. * @@ -10726,7 +10887,6 @@ export interface ListWebhooksOptions extends RecordActionOptions { sort?: 'ascending' | 'descending'; } - /** * Defines an interface that represents the options for a list action. * @@ -10742,7 +10902,6 @@ export interface ListNotificationsOptions extends RecordActionOptions { sort?: 'ascending' | 'descending'; } - export interface WebhookRecord extends CrudRecord { /** * The resource kind of the webhook target. @@ -10812,7 +10971,6 @@ export interface SubscribeToNotificationFailure { errorMessage: string; } - export type UnsubscribeToNotificationResult = | UnsubscribeToNotificationSuccess | UnsubscribeToNotificationFailure; @@ -10827,7 +10985,6 @@ export interface UnsubscribeToNotificationFailure { errorMessage: string; } - export type SendNotificationResult = | SendNotificationSuccess | SendNotificationFailure; @@ -10893,32 +11050,31 @@ export interface ListSubscriptionsFailure { errorMessage: string; } - export interface PushNotificationPayload { /** * The title of the push notification. - * + * * See https://web.dev/articles/push-notifications-display-a-notification#title_and_body_options */ title: string; /** * The body of the push notification. - * + * * See https://web.dev/articles/push-notifications-display-a-notification#title_and_body_options for more information. */ body?: string; /** * The URL to the icon that should displayed for the push notification. - * + * * See https://web.dev/articles/push-notifications-display-a-notification#icon for more information. */ icon?: string; /** * The URL to the image that should be used for the badge of the push notification. - * + * * See https://web.dev/articles/push-notifications-display-a-notification#badge for more information. */ badge?: string; @@ -10930,13 +11086,13 @@ export interface PushNotificationPayload { /** * The tag for the notification. - * + * * See https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#tag */ tag?: string; /** * The timestamp for the notification. - * + * * See https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#timestamp */ timestamp?: number; @@ -10952,7 +11108,9 @@ export interface PushNotificationPayload { actions?: PushNotificationActionOption[]; } -export type PushNotificationAction = PushNotificationOpenUrlAction | PushNotificationWebhookAction; +export type PushNotificationAction = + | PushNotificationOpenUrlAction + | PushNotificationWebhookAction; export interface PushNotificationOpenUrlAction { type: 'open_url'; @@ -11708,70 +11866,80 @@ interface Ai { /** * Generates a [skybox image](https://en.wikipedia.org/wiki/Skybox_%28video_games%29) from the given prompt. - * + * * Returns a promise that resolves with a URL to the generated image that can be used as the {@tag formAddress} of a bot that has {@tag form} set to `skybox`. - * + * * @param prompt the string that describes what the skybox should look like. * @param negativePrompt the string that describes what the skybox should avoid looking like. * @param options the additional options that should be used. - * + * * @example Generate a skybox from a prompt. * const skybox = await ai.generateSkybox("A skybox with a blue sky and green grass."); * masks.formAddress = skybox; - * + * * @example Generate a skybox from a prompt and a negative prompt * const skybox = await ai.generateSkybox("A skybox with a blue sky and green grass.", "A skybox with a red sky and brown grass."); * masks.formAddress = skybox; - * + * * @dochash actions/ai * @docname ai.generateSkybox * @docid ai.generateSkybox-string */ - generateSkybox(prompt: string, negativePrompt?: string, options?: AIGenerateSkyboxOptions): Promise; + generateSkybox( + prompt: string, + negativePrompt?: string, + options?: AIGenerateSkyboxOptions + ): Promise; /** * Generates a [skybox image](https://en.wikipedia.org/wiki/Skybox_%28video_games%29) from the given request object. - * + * * Returns a promise that resolves with an object that contains a URL to the generated image that can be used as the {@tag formAddress} of a bot that has {@tag form} set to `skybox`. - * + * * @param request the request object that describes what the skybox should look like. - * + * * @example Generate a skybox from a prompt. * const skybox = await ai.generateSkybox("A skybox with a blue sky and green grass."); * masks.formAddress = skybox; - * + * * @example Generate a skybox from a prompt and a negative prompt * const skybox = await ai.generateSkybox("A skybox with a blue sky and green grass.", "A skybox with a red sky and brown grass."); * masks.formAddress = skybox; - * + * * @dochash actions/ai * @docname ai.generateSkybox * @docid ai.generateSkybox-request */ - generateSkybox(request: AIGenerateSkyboxRequest): Promise; + generateSkybox( + request: AIGenerateSkyboxRequest + ): Promise; /** * Generates a [skybox image](https://en.wikipedia.org/wiki/Skybox_%28video_games%29) from the given request object. - * + * * Returns a promise that resolves with an object that contains a URL to the generated image that can be used as the {@tag formAddress} of a bot that has {@tag form} set to `skybox`. - * + * * @param request the request object that describes what the skybox should look like. * @param negativePrompt the string that describes what the skybox should avoid looking like. * @param options the additional options that should be used. - * + * * @example Generate a skybox from a prompt. * const skybox = await ai.generateSkybox("A skybox with a blue sky and green grass."); * masks.formAddress = skybox; - * + * * @example Generate a skybox from a prompt and a negative prompt * const skybox = await ai.generateSkybox("A skybox with a blue sky and green grass.", "A skybox with a red sky and brown grass."); * masks.formAddress = skybox; - * + * * @dochash actions/ai * @docname ai.generateSkybox * @docid ai.generateSkybox-request */ - generateSkybox(prompt: string | AIGenerateSkyboxRequest, negativePrompt?: string, options?: AIGenerateSkyboxOptions): Promise + generateSkybox( + prompt: string | AIGenerateSkyboxRequest, + negativePrompt?: string, + options?: AIGenerateSkyboxOptions + ): Promise; /** * Generates an image from the given prompt. @@ -11840,7 +12008,7 @@ interface Ai { /** * Gets an access token for the Hume AI API. * Returns a promise that resolves with the access token. - * + * * @example Get an access token for the Hume AI API. * const accessToken = await ai.hume.getAccessToken(); * @@ -11848,7 +12016,7 @@ interface Ai { * @docname ai.hume.getAccessToken */ getAccessToken(): Promise; - } + }; sloyd: { /** @@ -11856,14 +12024,17 @@ interface Ai { * @param request The options for the 3D model. * @param options The options for the request. */ - generateModel(request: AISloydGenerateModelOptions, options?: RecordActionOptions): Promise; - } + generateModel( + request: AISloydGenerateModelOptions, + options?: RecordActionOptions + ): Promise; + }; stream: { /** * Sends a chat message to the AI and streams the response back. * Returns a promise that resolves with an [async iterable](https://javascript.info/async-iterators-generators#async-iterables) that contains the responses from the AI. - * + * * Throws a {@link CasualOSError} if an error occurs while sending the message. * * This function can be useful for creating chat bots, or for using an Artificial Intelligence (AI) to process a message. @@ -11873,7 +12044,7 @@ interface Ai { * * @example Send a message to the AI and log the response. * const response = await ai.stream.chat("Hello!"); - * + * * for await (let message of response) { * console.log(message); * } @@ -11885,7 +12056,10 @@ interface Ai { * @docname ai.stream.chat * @docid ai.stream.chat-string */ - chat(message: string, options?: AIChatOptions): Promise>; + chat( + message: string, + options?: AIChatOptions + ): Promise>; /** * Sends a chat message to the AI and streams the response back. @@ -12028,14 +12202,14 @@ interface Ai { messages: string | AIChatMessage | AIChatMessage[], options?: AIChatOptions ): Promise>; - } + }; } interface Os { /** - * Sleeps for time in ms. - * @param time Time in ms. 1 second is 1000ms. - */ + * Sleeps for time in ms. + * @param time Time in ms. 1 second is 1000ms. + */ sleep(time: number): Promise; /** @@ -12164,10 +12338,10 @@ interface Os { /** * Attempts to show the "Account Info" dialog. * Does nothing if the user is not logged in. - * + * * @example Show the "Account Info" dialog. * await os.showAccountInfo(); - * + * * @dochash actions/os/system * @docname os.showAccountInfo */ @@ -12177,7 +12351,10 @@ interface Os { * Gets media permission for the device. * Promise will resolve if permission was granted, otherwise an error will be thrown. */ - getMediaPermission(options: { audio?: boolean, video?: boolean }): Promise; + getMediaPermission(options: { + audio?: boolean; + video?: boolean; + }): Promise; /** * Gets the current average frame rate for the 3D portals in seconds. @@ -12190,14 +12367,20 @@ interface Os { * @param roomName The name of the meeting room to join. * @param options The options for the meeting. */ - joinRoom(roomName: string, options?: JoinRoomActionOptions): Promise; + joinRoom( + roomName: string, + options?: JoinRoomActionOptions + ): Promise; - /** - * Attempts to leave the given meeting room. - * @param roomName The name of the meeting room to leave. - * @param options The options. - */ - leaveRoom(roomName: string, options?: LeaveRoomActionOptions): Promise; + /** + * Attempts to leave the given meeting room. + * @param roomName The name of the meeting room to leave. + * @param options The options. + */ + leaveRoom( + roomName: string, + options?: LeaveRoomActionOptions + ): Promise; /** * Attempts to set the options for the given meeting room. @@ -12205,7 +12388,10 @@ interface Os { * @param roomName The name of the room. * @param options The options to set. Omitted properties remain unchanged. */ - setRoomOptions(roomName: string, options: Partial): Promise; + setRoomOptions( + roomName: string, + options: Partial + ): Promise; /** * Gets the options for the given meeting room. @@ -12219,7 +12405,10 @@ interface Os { * @param roomName The name of the room. * @param address The address of the track that the options should be retrieved for. */ - getRoomTrackOptions(roomName: string, address: string): Promise; + getRoomTrackOptions( + roomName: string, + address: string + ): Promise; /** * Sets the options for the track with the given address in the given room. @@ -12227,14 +12416,21 @@ interface Os { * @param address The address of the track that the options should be retrieved for. * @param options The options that should be set for the track. */ - setRoomTrackOptions(roomName: string, address: string, options: SetRoomTrackOptions): Promise; + setRoomTrackOptions( + roomName: string, + address: string, + options: SetRoomTrackOptions + ): Promise; /** * Gets the options for the specified remote user in the specified room. * @param roomName The name of the room. * @param remoteId The ID of the remote user. */ - getRoomRemoteOptions(roomName: string, remoteId: string): Promise; + getRoomRemoteOptions( + roomName: string, + remoteId: string + ): Promise; /** * Gets the URL that AB1 should be bootstrapped from. @@ -12287,28 +12483,40 @@ interface Os { * @param controller The name of the controller that should be checked. * @param button The name of the button on the controller. */ - getInputState(controller: 'mousePointer', button: 'left' | 'right' | 'middle'): null | 'down' | 'held'; + getInputState( + controller: 'mousePointer', + button: 'left' | 'right' | 'middle' + ): null | 'down' | 'held'; /** * Gets the input state of the given button on the left or right controller. * @param controller The name of the controller that should be checked. * @param button The name of the button on the controller. */ - getInputState(controller: 'leftPointer' | 'rightPointer', button: 'primary' | 'squeeze'): null | 'down' | 'held'; + getInputState( + controller: 'leftPointer' | 'rightPointer', + button: 'primary' | 'squeeze' + ): null | 'down' | 'held'; /** * Gets the input state of the given button on the keyboard. * @param controller The name of the controller that should be checked. * @param button The name of the button on the controller. */ - getInputState(controller: 'keyboard', button: string): null | 'down' | 'held'; + getInputState( + controller: 'keyboard', + button: string + ): null | 'down' | 'held'; /** * Gets the input state of the given touch. * @param controller The name of the controller that should be checked. * @param button The index of the finger. */ - getInputState(controller: 'touch', button: '0' | '1' | '2' | '3' | '4'): null | 'down' | 'held'; + getInputState( + controller: 'touch', + button: '0' | '1' | '2' | '3' | '4' + ): null | 'down' | 'held'; /** * Gets the input state of the given button on the given controller. @@ -12327,7 +12535,10 @@ interface Os { * @param message The message to show. * @param duration The number of seconds the message should be on the screen. (Defaults to 2) */ - toast(message: string | number | boolean | object | Array | null, duration?: number): ShowToastAction; + toast( + message: string | number | boolean | object | Array | null, + duration?: number + ): ShowToastAction; /** * Shows a tooltip message to the user. @@ -12336,13 +12547,18 @@ interface Os { * @param pixelY The Y coordinate that the tooltip should be shown at. If null, then the current pointer position will be used. * @param duration The duration that the tooltip should be shown in seconds. */ - tip(message: string | number | boolean | object | Array | null, pixelX?: number, pixelY?: number, duration?: number): Promise; + tip( + message: string | number | boolean | object | Array | null, + pixelX?: number, + pixelY?: number, + duration?: number + ): Promise; /** * Hides the given list of tips. * If no tip IDs are provided, then all tips will be hidden. - * @param tipIds - * @returns + * @param tipIds + * @returns */ hideTips(tipIds?: number | number[]): Promise; @@ -12350,7 +12566,7 @@ interface Os { * Play the given url's audio. * Returns a promise that resolves with the sound ID when the sound starts playing. * @param url The URL to play. - * + * * @example * // Play a cow "moo" * os.playSound("https://freesound.org/data/previews/58/58277_634166-lq.mp3"); @@ -12361,7 +12577,7 @@ interface Os { * Preloads the audio for the given URL. * Returns a promise that resolves when the audio has finished loading. * @param url The URl to preload. - * + * * @example * // Preload a cow "moo" * os.bufferSound("https://freesound.org/data/previews/58/58277_634166-lq.mp3"); @@ -12396,7 +12612,7 @@ interface Os { /** * Sends a command to the Jitsi Meet API. * See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-iframe/#commands for a list of commands. - * + * * Returns a promise that resolves when the command has been executed. * @param command The name of the command to execute. * @param args The arguments for the command (if any). @@ -12416,10 +12632,7 @@ interface Os { * @param inst The inst that should be joined. Defaults to the current inst. * @param dimension The dimension that should be joined. Defaults to the current dimension. */ - showJoinCode( - inst?: string, - dimension?: string - ): ShowJoinCodeAction; + showJoinCode(inst?: string, dimension?: string): ShowJoinCodeAction; /** * Requests that AUX enters fullscreen mode. @@ -12461,7 +12674,10 @@ interface Os { * @param bot The bot. * @param targets The targets that should be enabled when the bot is being dropped on. */ - addBotDropSnap(bot: Bot | string, ...targets: SnapTarget[]): AddDropSnapTargetsAction; + addBotDropSnap( + bot: Bot | string, + ...targets: SnapTarget[] + ): AddDropSnapTargetsAction; /** * Adds the given list of grids to the current drag operation. @@ -12474,7 +12690,10 @@ interface Os { * @param bot The bot. * @param targets The list of grids to add. */ - addBotDropGrid(bot: Bot | string, ...targets: SnapGridTarget[]): AddDropGridTargetsAction; + addBotDropGrid( + bot: Bot | string, + ...targets: SnapGridTarget[] + ): AddDropGridTargetsAction; /** * Enables custom dragging for the current drag operation. @@ -12513,7 +12732,7 @@ interface Os { * @param options The options to use for moving the camera. */ focusOn( - botOrPosition: Bot | string | { x: number, y: number, z?: number; }, + botOrPosition: Bot | string | { x: number; y: number; z?: number }, options: FocusOnOptions ): Promise; @@ -12578,7 +12797,7 @@ interface Os { * * @example Close the photo camera * await os.closePhotoCamera(); - * + * * @dochash actions/os/camera * @docname os.closePhotoCamera */ @@ -12609,14 +12828,14 @@ interface Os { /** * Captures a screenshot (i.e. photo/picture) from the grid portal. - * + * * Returns a promise that resolves with the captured photo. - * + * * @param portal the portal to capture the screenshot from. Defaults to the grid portal. - * + * * @example Capture a screenshot from the grid portal. * const screenshot = await os.capturePortalScreenshot(); - * + * * @dochash actions/os/portals * @docname os.capturePortalScreenshot */ @@ -12649,9 +12868,9 @@ interface Os { /** * Classifies the given images using the image classifier. Returns a promise that resolves with the results of the classification. - * + * * @param options the options that should be used for the image classification. - * + * * @example Classify the given images. * const files = await os.showUploadFiles() * const classify = os.classifyImages({ @@ -12660,12 +12879,14 @@ interface Os { * return {file} * }) * }) - * + * * @dochash actions/os/image-classification * @docname os.classifyImages * @docgroup 10-image-classifier - */ - classifyImages(options: ClassifyImagesOptions): Promise + */ + classifyImages( + options: ClassifyImagesOptions + ): Promise; /** * Gets the local device time in miliseconds since January 1st 1970 UTC-0. @@ -12879,10 +13100,12 @@ interface Os { * type: "text" * }); */ - showInput: MaskFunc<( - currentValue?: any, - options?: Partial - ) => Promise>; + showInput: MaskFunc< + ( + currentValue?: any, + options?: Partial + ) => Promise + >; /** * Shows a confirmation dialog. Returns a promise that resolves with true if the "Confirm" button is clicked and false if the "Cancel" button is clicked or the dialog is closed. @@ -12933,23 +13156,18 @@ interface Os { /** * Registers a custom app for the given bot with the given options. * Apps allow you add custom functionality to the CasualOS frontend and are deeply integrated into the CasualOS platform. - * + * * @param id The ID of the app. * @param bot The bot that should be used to control the app. */ - registerApp( - id: string, - bot: Bot | string - ): Promise; + registerApp(id: string, bot: Bot | string): Promise; /** * Removes a custom app from the session. - * + * * @param id The ID of the app. */ - unregisterApp( - id: string - ): Promise; + unregisterApp(id: string): Promise; /** * Sets the output of the given app. @@ -12968,10 +13186,7 @@ interface Os { /** * Returns a stateful value, and a function to update it. */ - useState(): [ - S | undefined, - StateUpdater - ]; + useState(): [S | undefined, StateUpdater]; /** * An alternative to `useState`. @@ -13087,9 +13302,11 @@ interface Os { * @param parent The element that the VDOM should be added inside of. * @param replaceNode The element that should be replaced. Can be used as a performance optimization if you know which element was changed by the VDOM. */ - render(vdom: any, + render( + vdom: any, parent: Element | Document | ShadowRoot | DocumentFragment, - replaceNode?: Element | Text): void; + replaceNode?: Element | Text + ): void; /** * Creates a hook that can be used to get a reference to the HTML element that a Preact component is attached to. @@ -13152,12 +13369,12 @@ interface Os { /** * Shows the "report inst" dialog to the user. - * + * * Returns a promise that resolves once the dialog has been closed. - * + * * @example Show the "report inst" dialog. * await os.reportInst(); - * + * * @dochash actions/os/moderation * @docname os.reportInst */ @@ -13173,9 +13390,9 @@ interface Os { * On success, the `authBot` global variable will reference the bot that was returned by the promise. * * See [Auth Bot Tags](page:tags#auth-bot-tags) for more information. - * + * * See {@link os.requestAuthBotInBackground} for a version of this function that does not show a popup if the user is not signed in. - * + * * @example Request an auth bot for the user * await os.requestAuthBot(); * os.toast("Logged in!"); @@ -13192,7 +13409,7 @@ interface Os { /** * Requests that an "authentication" bot be added to the inst for the current browser tab. * Works similarly to {@link os.requestAuthBot}, except that the request will not show a popup if the user is not signed in. - * + * * Auth bots are useful for discovering general information about the logged in user and are typically associated with a [https://publicos.link](https://publicos.link) user account. * * Returns a promise that resolves with a bot that contains information about the signed in user session. @@ -13201,7 +13418,7 @@ interface Os { * On success, the `authBot` global variable will reference the bot that was returned by the promise. * * See [Auth Bot Tags](page:tags#auth-bot-tags) for more information. - * + * * See {@link os.requestAuthBot} for a version of this function that shows a popup if the user is not signed in. * * @example Request the auth bot in the background. @@ -13225,7 +13442,9 @@ interface Os { * Gets an access key for the given public record. * @param name The name of the record. */ - getPublicRecordKey(recordName: string): Promise; + getPublicRecordKey( + recordName: string + ): Promise; /** * Gets a subjectless access key for the given public record. @@ -13234,7 +13453,7 @@ interface Os { getSubjectlessPublicRecordKey( recordName: string ): Promise; - + /** * Grants the given permission in the given record. * @@ -13381,7 +13600,11 @@ interface Os { * @param startingAddress The address that the list should start with. * @param endpoint The records endpoint that should be queried. Optional. */ - listData(recordKeyOrName: string, startingAddress?: string, endpoint?: string): Promise; + listData( + recordKeyOrName: string, + startingAddress?: string, + endpoint?: string + ): Promise; /** * Gets a partial list of [data](glossary:data-record) with the given marker that is stored in the given record. @@ -13396,7 +13619,7 @@ interface Os { * Since items are ordered within the record by address, this can be used as way to iterate through all the data items in a record. * If omitted, then the list will start with the first item. * @param options The options for the operation. - * + * * @example Get a list of publicRead data items in a record * const result = await os.listData('myRecord', 'publicRead'); * if (result.success) { @@ -13424,7 +13647,7 @@ interface Os { * break; * } * } - * + * * @example List publicRead items in descending order * const result = await os.listData('myRecord', 'publicRead', null, { sort: 'descending' }); * if (result.success) { @@ -13432,7 +13655,7 @@ interface Os { * } else { * os.toast("Failed " + result.errorMessage); * } - * + * * @dochash actions/os/records * @docgroup 01-records * @docname os.listDataByMarker @@ -13450,12 +13673,16 @@ interface Os { * @param address The address that the data should be erased from. * @param endpoint The records endpoint that should be queried. Optional. */ - eraseData(recordKey: string, address: string, endpoint?: string): Promise; + eraseData( + recordKey: string, + address: string, + endpoint?: string + ): Promise; /** * Records the given data to the given address inside the record for the given record key. * This data needs to be manually approved when reading, writing, or erasing it. - * + * * @param recordKey The key that should be used to access the record. * @param address The address that the data should be stored at inside the record. * @param data The data that should be stored. @@ -13471,7 +13698,7 @@ interface Os { /** * Gets the data stored in the given record at the given address. * This data needs to be manually approved when reading, writing, or erasing it. - * + * * @param recordKeyOrName The record that the data should be retrieved from. * @param address The address that the data is stored at. * @param endpoint The records endpoint that should be queried. Optional. @@ -13485,14 +13712,17 @@ interface Os { /** * Erases the data stored in the given record at the given address. * This data needs to be manually approved when reading, writing, or erasing it. - * + * * @param recordKey The key that should be used to access the record. * @param address The address that the data should be erased from. * @param endpoint The records endpoint that should be queried. Optional. */ - eraseManualApprovalData(recordKey: string, address: string, endpoint?: string): Promise; + eraseManualApprovalData( + recordKey: string, + address: string, + endpoint?: string + ): Promise; - /** * Creates or updates a [webhook](glossary:webhook-record) in the given record using the given options. * @@ -13536,22 +13766,27 @@ interface Os { webhook: WebhookRecord, options?: RecordActionOptions ): Promise; - + /** * Runs the webhook in the given record with the provided input. * @param recordName the name of the record. * @param address the address of the webhook. * @param input the input to provide to the webhook. * @param options the options to use. - * + * * @example Run a webhook with some input. * const result = await os.runWebhook('myRecord', 'myWebhookAddress', { myInput: 'myValue' }); - * + * * @dochash actions/os/records * @docgroup 05-records * @docname os.runWebhook */ - runWebhook(recordName: string, address: string, input: any, options?: RecordActionOptions): Promise; + runWebhook( + recordName: string, + address: string, + input: any, + options?: RecordActionOptions + ): Promise; /** * Gets the [webhook](glossary:webhook-record) from the given record. @@ -13802,14 +14037,14 @@ interface Os { /** * Gets the list of subscriptions for the given notification in the given record. - * + * * @param recordName the name of the record. * @param address the address of the notification. * @param options the options to use. - * + * * @example List notification subscriptions. * const result = await os.listNotificationSubscriptions('myRecord', 'myNotificationAddress'); - * + * * @dochash actions/os/records * @docgroup 06-records * @docname os.listNotificationSubscriptions @@ -13822,12 +14057,12 @@ interface Os { /** * Gets the list of notification subscriptions for the current user. - * + * * @param options the options to use. - * + * * @example List the current user's notification subscriptions. * const result = await os.listUserNotificationSubscriptions(); - * + * * @dochash actions/os/records * @docgroup 06-records * @docname os.listUserNotificationSubscriptions @@ -13898,10 +14133,7 @@ interface Os { * @param result The successful result of a os.recordFile() call. * @param endpoint The endpoint that should be queried. Optional. */ - getPrivateFile( - result: RecordFileSuccess, - endpoint?: string - ): Promise; + getPrivateFile(result: RecordFileSuccess, endpoint?: string): Promise; /** * Gets the data stored in the given private file. @@ -13926,21 +14158,33 @@ interface Os { * @param result The successful result of a os.recordFile() call. * @param endpoint The records endpoint that should be queried. Optional. */ - eraseFile(recordKey: string, result: RecordFileSuccess, endpoint?: string): Promise; + eraseFile( + recordKey: string, + result: RecordFileSuccess, + endpoint?: string + ): Promise; /** * Deletes the specified file using the given record key. * @param recordKey The key that should be used to delete the file. * @param url The URL that the file is stored at. * @param endpoint The records endpoint that should be queried. Optional. */ - eraseFile(recordKey: string, url: string, endpoint?: string): Promise; + eraseFile( + recordKey: string, + url: string, + endpoint?: string + ): Promise; /** * Deletes the specified file using the given record key. * @param recordKey The key that should be used to delete the file. * @param urlOrRecordFileResult The URL or the successful result of the record file operation. * @param endpoint The records endpoint that should be queried. Optional. */ - eraseFile(recordKey: string, fileUrlOrRecordFileResult: string | RecordFileSuccess, endpoint?: string): Promise; + eraseFile( + recordKey: string, + fileUrlOrRecordFileResult: string | RecordFileSuccess, + endpoint?: string + ): Promise; /** * Records that the given event occurred. @@ -13948,7 +14192,11 @@ interface Os { * @param eventName The name of the event. * @param endpoint The records endpoint that should be queried. Optional. */ - recordEvent(recordKey: string, eventName: string, endpoint?: string): Promise; + recordEvent( + recordKey: string, + eventName: string, + endpoint?: string + ): Promise; /** * Gets the number of times that the given event has been recorded. @@ -13956,24 +14204,28 @@ interface Os { * @param eventName The name of the event. * @param endpoint The records endpoint that should be queried. Optional. */ - countEvents(recordNameOrKey: string, eventName: string, endpoint?: string): Promise; + countEvents( + recordNameOrKey: string, + eventName: string, + endpoint?: string + ): Promise; /** * Gets the list of studios that the currently logged in user has access to. - * + * * Returns a promise that resolves with an object that contains the list of studios (if successful) or information about the error that occurred. - * + * * @param endpoint the HTTP Endpoint of the records website that the data should be retrieved from. If omitted, then the preconfigured records endpoint will be used. Note that when using a custom endpoint, the record key must be a valid record key for that endpoint. - * + * * @example Get the list of studios that the user has access to * const result = await os.listUserStudios(); - * + * * if (result.success) { * os.toast(result.studios.map(s => s.name).join(', ')); * } else { * os.toast('Failed to get studios ' + result.errorMessage); * } - * + * * @dochash actions/os/records * @docgroup 01-records * @docname os.listUserStudios @@ -13982,11 +14234,11 @@ interface Os { /** * Gets the default records endpoint. That is, the records endpoint that is used for records actions when no endpoint is specified. - * + * * @example Get the default records endpoint. * const endpoint = await os.getRecordsEndpoint(); * os.toast("The default records endpoint is: " + endpoint); - * + * * @dochash actions/os/records * @docname os.getRecordsEndpoint */ @@ -13996,14 +14248,19 @@ interface Os { * Converts the given geolocation to a what3words (https://what3words.com/) address. * @param location The latitude and longitude that should be converted to a 3 word address. */ - convertGeolocationToWhat3Words(location: ConvertGeolocationToWhat3WordsOptions): Promise; + convertGeolocationToWhat3Words( + location: ConvertGeolocationToWhat3WordsOptions + ): Promise; /** * Casts a 3D ray into the specified portal from the camera and returns information about the bots that were hit. * @param portal The portal that should be tested. * @param viewportCoordinates The 2D camera viewport coordinates that the ray should be sent from. */ - raycastFromCamera(portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', viewportCoordinates: Vector2): Promise; + raycastFromCamera( + portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', + viewportCoordinates: Vector2 + ): Promise; /** * Casts a 3D ray into the specified portal using the given origin and direction and returns information about the bots that were hit. @@ -14011,27 +14268,34 @@ interface Os { * @param origin The 3D point that the ray should start at. * @param direction The 3D direction that the ray should travel in. */ - raycast(portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', origin: Vector3, direction: Vector3): Promise; + raycast( + portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', + origin: Vector3, + direction: Vector3 + ): Promise; /** * Calculates the 3D ray that would be projected into the given portal based on the specified camera viewport coordinates. * @param portal The portal that the ray should be projected into. * @param viewportCoordinates The 2D camera viewport coordinates that the ray should be sent from. */ - calculateRayFromCamera(portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', viewportCoordinates: Vector2): Promise; + calculateRayFromCamera( + portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', + viewportCoordinates: Vector2 + ): Promise; /** * Calculates the viewport coordinates that the given position would be projected to in the camera of the given portal. * Returns a promise that resolves with the calculated viewport coordinates. - * + * * Viewport coordinates locate a specific point on the image that the camera produces. * `(X: 0, Y: 0)` represents the center of the camera while `(X: -1, Y: -1)` represents the lower left corner and `(X: 1, Y: 1)` represents the upper right corner. - * + * * This function is useful for converting a position in the portal to a position on the camera viewport (screen position, but the location is not in pixels). - * + * * @param portal the name of the portal that should be tested. * @param position the 3D position that should be projected to the viewport. - * + * * @example Calculate the viewport coordinates of the current bot in the home dimension in the grid portal * const botPosition = new Vector3( * thisBot.homeX, @@ -14041,49 +14305,58 @@ interface Os { * const viewportPosition = await os.calculateViewportCoordinatesFromPosition("grid", botPosition); * os.toast(viewportPosition); */ - calculateViewportCoordinatesFromPosition(portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', position: Vector3): Promise; + calculateViewportCoordinatesFromPosition( + portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', + position: Vector3 + ): Promise; /** * Calculates the screen coordinates that the given viewport coordinates map to on the screen. * Returns a promise that resolves with the calculated screen coordinates. - * + * * Screen coordinates are in pixels and are relative to the top-left corner of the screen. * Viewport coordinates locate a specific point on the image that the camera produces. * `(X: 0, Y: 0)` represents the center of the camera while `(X: -1, Y: -1)` represents the lower left corner and `(X: 1, Y: 1)` represents the upper right corner. - * + * * @param portal the name of the portal that should be tested. * @param coordinates the 2D viewport coordinates that should be converted to screen coordinates. - * + * * @example Calculate the screen coordinates at the center of grid portal screen * const screenCoordinates = await os.calculateScreenCoordinatesFromViewportCoordinates('grid', new Vector2(0, 0)); * os.toast(screenCoordinates); - * + * * @dochash actions/os/portals * @docname os.calculateScreenCoordinatesFromViewportCoordinates * @docgroup 10-raycast */ - calculateScreenCoordinatesFromViewportCoordinates(portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', coordinates: Vector2): Promise; + calculateScreenCoordinatesFromViewportCoordinates( + portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', + coordinates: Vector2 + ): Promise; /** * Calculates the viewport coordinates that the given screen coordinates map to on the camera. * Returns a promise that resolves with the calculated viewport coordinates. - * + * * Screen coordinates are in pixels and are relative to the top-left corner of the screen. * Viewport coordinates locate a specific point on the image that the camera produces. * `(X: 0, Y: 0)` represents the center of the camera while `(X: -1, Y: -1)` represents the lower left corner and `(X: 1, Y: 1)` represents the upper right corner. - * + * * @param portal the name of the portal that should be tested. * @param coordinates the 2D screen coordinates that should be converted to viewport coordinates. - * + * * @example Calculate the viewport coordinates at (0,0) on the screen * const viewportCoordinates = await os.calculateViewportCoordinatesFromScreenCoordinates('grid', new Vector2(0, 0)); * os.toast(viewportCoordinates); - * + * * @dochash actions/os/portals * @docname os.calculateViewportCoordinatesFromScreenCoordinates * @docgroup 10-raycast */ - calculateViewportCoordinatesFromScreenCoordinates(portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', coordinates: Vector2): Promise; + calculateViewportCoordinatesFromScreenCoordinates( + portal: 'grid' | 'miniGrid' | 'map' | 'miniMap', + coordinates: Vector2 + ): Promise; /** * Requests that the given address be pre-cached so that it is available for use on a bot. @@ -14099,7 +14372,11 @@ interface Os { * @param nameOrIndex The name or index of the animation that should be started. * @param options The options that should be used for the animation. */ - startFormAnimation(bot: Bot, nameOrIndex: string | number, options?: StartFormAnimationOptions): Promise; + startFormAnimation( + bot: Bot, + nameOrIndex: string | number, + options?: StartFormAnimationOptions + ): Promise; /** * Starts the given animation on the given bot(s). * Returns a promise that resolves once the animation(s) have been started. @@ -14107,7 +14384,11 @@ interface Os { * @param nameOrIndex The name of the animation. * @param options The options for the animation. */ - startFormAnimation(botOrBots: Bot | string | (Bot | string)[], nameOrIndex: string | number, options?: StartFormAnimationOptions): Promise; + startFormAnimation( + botOrBots: Bot | string | (Bot | string)[], + nameOrIndex: string | number, + options?: StartFormAnimationOptions + ): Promise; /** * Stops the animation on the given bot(s). @@ -14115,30 +14396,35 @@ interface Os { * @param botOrBots The bot or list of bots that the animation(s) should be stopped on. * @param options The options that should be used. */ - stopFormAnimation(botOrBots: Bot | string | (Bot | string)[], options?: StopFormAnimationOptions): Promise; + stopFormAnimation( + botOrBots: Bot | string | (Bot | string)[], + options?: StopFormAnimationOptions + ): Promise; /** * Gets the list of animations that are included in the given the form or bot. * @param botOrAddress The bot, bot ID, or address that the animations should be retrieved from. */ - listFormAnimations(botOrAddress: Bot | string): Promise; + listFormAnimations( + botOrAddress: Bot | string + ): Promise; /** * Counts the number of build steps that exist in the given lego [LDraw](https://ldraw.org/) file. * Returns a promise that resolves with the number of build steps. - * + * * @param address The address of the file. - * + * * @example Count the number of build steps in an example LDraw file * const steps = await os.ldrawCountAddressBuildSteps('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/models/ldraw/officialLibrary/models/car.ldr_Packed.mpd'); * os.toast("There are " + steps + " build steps in the file."); - * + * * @example Animate the build steps of a bot * const steps = await os.ldrawCountAddressBuildSteps('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/models/ldraw/officialLibrary/models/car.ldr_Packed.mpd'); * for (let i = 0; i < steps; i++) { * masks.formBuildStep = i; * } - * + * * @dochash actions/os/ldraw * @docname os.ldrawCountTextBuildSteps */ @@ -14147,15 +14433,15 @@ interface Os { /** * Counts the number of build steps that exist in the given lego [LDraw](https://ldraw.org/) file. * Returns a promise that resolves with the number of build steps. - * + * * @param text The text content of the file. - * + * * @example Count the number of build steps in an uploaded LDraw file * const files = await os.showUploadFiles(); * const file = files[0]; * const steps = await os.ldrawCountTextBuildSteps(file.data); * os.toast("There are " + steps + " build steps in the file."); - * + * * @dochash actions/os/ldraw * @doctitle LDraw Actions * @docsidebar LDraw @@ -14169,7 +14455,10 @@ interface Os { * @param prefix The prefix that code tags should start with. * @param options The options that should be used for the prefix. */ - registerTagPrefix(prefix: string, options?: RegisterPrefixOptions): Promise; + registerTagPrefix( + prefix: string, + options?: RegisterPrefixOptions + ): Promise; /** * Gets the number of devices that are viewing the current inst. @@ -14205,8 +14494,8 @@ interface Os { getSharedDocument(recordOrName: string, inst?: string, name?: string): Promise; /** - * Gets the list of remote IDs that are connected to the inst. - */ + * Gets the list of remote IDs that are connected to the inst. + */ remotes(): Promise; /** @@ -14226,7 +14515,7 @@ interface Os { * This is valuable for situations where you want to ensure that all players observe the same state. * @param bots The bots. */ - createInitializationUpdate(bots: Bot[]): Promise; + createInitializationUpdate(bots: Bot[]): Promise; /** * Applies the given updates to the inst. @@ -14238,7 +14527,7 @@ interface Os { /** * Gets the current inst update. - * + * * This function is somewhat equivalent to calling os.listInstUpdates() followed by os.mergeInstUpdates(), but it uses the locally available state instead of fetching from the server. * This makes it more efficient as well as usable even when the server is not available. */ @@ -14251,8 +14540,8 @@ interface Os { mergeInstUpdates(updates: InstUpdate[]): InstUpdate; /** - * Gets the total number of devices that are connected to the server. - */ + * Gets the total number of devices that are connected to the server. + */ totalRemoteCount(): Promise; /** @@ -14263,10 +14552,12 @@ interface Os { /** * Gets the list of instances that are on the server. */ - instStatuses(): Promise<{ - inst: string, - lastUpdateTime: Date - }[]>; + instStatuses(): Promise< + { + inst: string; + lastUpdateTime: Date; + }[] + >; /** * Creates a new debugger that can be used to test and simulate bots. @@ -14294,7 +14585,10 @@ interface Os { * Sends an event to attach the given debugger to the CasualOS frontend. * @param debug The debugger that should be attached. */ - attachDebugger(debug: Debugger, options?: AttachDebuggerOptions): Promise; + attachDebugger( + debug: Debugger, + options?: AttachDebuggerOptions + ): Promise; /** * Sends an event to detach the given debugger from the CasualOS frontend. @@ -14309,7 +14603,6 @@ interface Os { } interface Server { - /** * Executes the given shell script on the server. * @param script The shell script that should be executed. @@ -14329,25 +14622,28 @@ interface Server { /** * Gets the list of instances that are on the server. */ - instStatuses(): Promise<{ - inst: string, - lastUpdateTime: Date - }[]>; -}; + instStatuses(): Promise< + { + inst: string; + lastUpdateTime: Date; + }[] + >; +} interface Web { - /** * Sends an HTTP GET request for the given URL using the given options. * @param url The URL to request. * @param options The options to use. - * + * * @example - * + * * // Send a HTTP GET request for https://www.example.com * const result = await web.get('https://www.example.com'); */ - get: MaskFunc<(url: string, options?: WebhookOptions) => Promise>; + get: MaskFunc< + (url: string, options?: WebhookOptions) => Promise + >; /** * Sends a HTTP POST request to the given URL with the given data. @@ -14357,32 +14653,66 @@ interface Web { * @param options The options that should be included in the request. * * @example - * + * * // Send a HTTP POST request to https://www.example.com/api/createThing * const result = await web.post('https://www.example.com/api/createThing', { * hello: 'world' * }); - * + * */ - post: MaskFunc<(url: string, data?: any, options?: WebhookOptions) => Promise>; + post: MaskFunc< + ( + url: string, + data?: any, + options?: WebhookOptions + ) => Promise + >; /** * Sends a web request based on the given options. * @param options The options that specify where and what to send in the web request. * * @example - * + * * // Send a HTTP GET request to https://example.com * const result = await web.hook({ * method: 'GET', * url: 'https://example.com' * }); - * + * * os.toast(result); */ hook: MaskFunc<(options: WebhookOptions) => Promise>; -}; +} +/** + * The interface that provides programmatic access to the Xp system in CasualOS. + */ +interface Xp { + /** + * Gets the user meta for the given user. + * * Defaults to the current logged in user if no user is specified. + * @param useId Search criteria for getting the associated xp user. + */ + getUserMeta(useId?: { + /** + * The id of the auth user to get the xp meta for. + * (Mutually exclusive with xpId) + */ + userId?: string; + /** + * The id of the xp user to get the xp meta for. + * (Mutually exclusive with userId) + */ + xpId?: string; + }): Promise; + /** + * Gets the user meta for the given user. + * * Defaults to the current logged in user if no user is specified. + * @param userId The id of the auth user to get the xp meta for. + */ + getUserMeta(userId?: string): Promise; +} interface Actions { /** @@ -14396,7 +14726,7 @@ interface Actions { * @param action The action to reject. */ reject(action: any): BotAction; -}; +} interface Math { /** @@ -14440,7 +14770,9 @@ interface Math { * Creates a new random number generator and returns it. * @param seed The value that should be used to seed the random number generator. */ - getSeededRandomNumberGenerator(seed?: number | string): PseudoRandomNumberGenerator; + getSeededRandomNumberGenerator( + seed?: number | string + ): PseudoRandomNumberGenerator; /** * Sets the seed that should be used for random numbers. @@ -14476,7 +14808,12 @@ interface Math { * @param planeNormal The direction that the face of the plane is pointing. * @param planeOrigin The position that the center of the plane should pass through. */ - intersectPlane(origin: Point3D, direction: Point3D, planeNormal?: Point3D, planeOrigin?: Point3D): Vector3; + intersectPlane( + origin: Point3D, + direction: Point3D, + planeNormal?: Point3D, + planeOrigin?: Point3D + ): Vector3; /** * Gets the position offset for the given bot anchor point. @@ -14521,7 +14858,7 @@ interface Math { * @param scale The number that the vector should be multiplied by. */ scaleVector(vector: T, scale: number): T; -}; +} interface ModFuncs { /** @@ -14529,9 +14866,9 @@ interface ModFuncs { * @param point The mod that represents the 3D point. */ cameraPositionOffset(point: Partial): { - cameraPositionOffsetX: number, - cameraPositionOffsetY: number, - cameraPositionOffsetZ: number, + cameraPositionOffsetX: number; + cameraPositionOffsetY: number; + cameraPositionOffsetZ: number; }; /** @@ -14539,11 +14876,11 @@ interface ModFuncs { * @param rotation The mod that represents the 3D rotation. */ cameraRotationOffset(rotation: Partial): { - cameraRotationOffsetX: number, - cameraRotationOffsetY: number, - cameraRotationOffsetZ: number, + cameraRotationOffsetX: number; + cameraRotationOffsetY: number; + cameraRotationOffsetZ: number; }; -}; +} interface Bytes { /** @@ -14552,10 +14889,10 @@ interface Bytes { */ toBase64String(bytes: Uint8Array): string; - /** - * Converts the given base64 formatted string into an array of bytes. - * @param base64 The base64 that should be converted to bytes. - */ + /** + * Converts the given base64 formatted string into an array of bytes. + * @param base64 The base64 that should be converted to bytes. + */ fromBase64String(base64: string): Uint8Array; /** @@ -14575,13 +14912,13 @@ interface Bytes { * @param bytes The data that should be converted to a Base64 Data URL. If given a string, then it should be valid Base 64 data. * @param mimeType The [MIME Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) of the data. * If omitted, then `image/png` will be used. - * + * * @example Convert some bytes to a base 64 image/png data URL * const data = bytes.toBase64Url(new Uint8Array([ 255, 254, 253 ])); - * + * * @example Convert a base 64 string to a text/plain base 64 data URL * const data = bytes.toBase64Url('aGVsbG8=', 'text/plain'); // "hello" encoded in Base64 - * + * * @dochash actions/bytes * @docname bytes.toBase64Url */ @@ -14589,14 +14926,14 @@ interface Bytes { /** * Converts the given [Data URL](https://developer.mozilla.org/en-US/docs/web/http/basics_of_http/data_urls) into a blob object. - * + * * Returns a blob that contains the binary data. Returns null if the URL is not a valid Data URL. - * + * * @param url The URL. - * + * * @example Convert a data URL to a blob * const blob = bytes.fromBase64Url('data:image/png;base64,aGVsbG8='); // "hello" encoded in Base64 - * + * * @dochash actions/bytes * @docname bytes.fromBase64Url */ @@ -14604,7 +14941,6 @@ interface Bytes { } interface Crypto { - /** * Calculates the cryptographic hash for the given data and returns the result in the specified format. * @param algorithm The algorithm that should be used to hash the data. @@ -14614,7 +14950,11 @@ interface Crypto { * - "raw" indicates that an array of bytes should be returned. * @param data The data that should be hashed. */ - hash(algorithm: 'sha256' | 'sha512' | 'sha1', format: 'hex' | 'base64', ...data: unknown[]): string; + hash( + algorithm: 'sha256' | 'sha512' | 'sha1', + format: 'hex' | 'base64', + ...data: unknown[] + ): string; /** * Calculates the cryptographic hash for the given data and returns the result in the specified format. @@ -14625,8 +14965,12 @@ interface Crypto { * - "raw" indicates that an array of bytes should be returned. * @param data The data that should be hashed. */ - hash(algorithm: 'sha256' | 'sha512' | 'sha1', format: 'raw', ...data: unknown[]): Uint8Array; - + hash( + algorithm: 'sha256' | 'sha512' | 'sha1', + format: 'raw', + ...data: unknown[] + ): Uint8Array; + /** * Calculates the cryptographic hash for the given data and returns the result in the specified format. * @param algorithm The algorithm that should be used to hash the data. @@ -14636,7 +14980,11 @@ interface Crypto { * - "raw" indicates that an array of bytes should be returned. * @param data The data that should be hashed. */ - hash(algorithm: 'sha256' | 'sha512' | 'sha1', format: 'hex' | 'base64' | 'raw', ...data: unknown[]): string | Uint8Array; + hash( + algorithm: 'sha256' | 'sha512' | 'sha1', + format: 'hex' | 'base64' | 'raw', + ...data: unknown[] + ): string | Uint8Array; /** * Calculates the HMAC of the given data and returns the result in the specified format. @@ -14649,7 +14997,12 @@ interface Crypto { * @param key The key that should be used to sign the message. * @param data The data that should be hashed. */ - hmac(algorithm: 'hmac-sha256' | 'hmac-sha512' | 'hmac-sha1', format: 'hex' | 'base64', key: string, ...data: unknown[]): string; + hmac( + algorithm: 'hmac-sha256' | 'hmac-sha512' | 'hmac-sha1', + format: 'hex' | 'base64', + key: string, + ...data: unknown[] + ): string; /** * Calculates the HMAC of the given data and returns the result in the specified format. @@ -14662,8 +15015,13 @@ interface Crypto { * @param key The key that should be used to sign the message. * @param data The data that should be hashed. */ - hmac(algorithm: 'hmac-sha256' | 'hmac-sha512' | 'hmac-sha1', format: 'raw', key: string, ...data: unknown[]): Uint8Array; - + hmac( + algorithm: 'hmac-sha256' | 'hmac-sha512' | 'hmac-sha1', + format: 'raw', + key: string, + ...data: unknown[] + ): Uint8Array; + /** * Calculates the HMAC of the given data and returns the result in the specified format. * HMAC is commonly used to verify that a message was created with a specific key. @@ -14675,7 +15033,12 @@ interface Crypto { * @param key The key that should be used to sign the message. * @param data The data that should be hashed. */ - hmac(algorithm: 'hmac-sha256' | 'hmac-sha512' | 'hmac-sha1', format: 'hex' | 'base64' | 'raw', key: string, ...data: unknown[]): string | Uint8Array + hmac( + algorithm: 'hmac-sha256' | 'hmac-sha512' | 'hmac-sha1', + format: 'hex' | 'base64' | 'raw', + key: string, + ...data: unknown[] + ): string | Uint8Array; /** * Calculates the SHA-256 hash of the given data. @@ -14711,19 +15074,19 @@ interface Crypto { /** * Encrypts the given data with the given secret and returns the result. - * + * * @description Always choose a strong unique secret. Use a password manager such as LastPass or 1Password to * help you create and keep track of them. - * + * * Assuming the above, this method will return a string of encrypted data that is confidential (unreadable without the secret), * reliable (the encrypted data cannot be changed without making it unreadable), and authentic (decryptability proves that the secret was used to encrypt the data). - * + * * As a consequence, encrypting the same data with the same secret will produce different results. * This is to ensure that an attacker cannot correlate different pieces of data to potentially deduce the original plaintext. - * - * Encrypts the given data using an authenticated encryption mechanism + * + * Encrypts the given data using an authenticated encryption mechanism * based on XSalsa20 (An encryption cipher) and Poly1305 (A message authentication code). - * + * * @param secret The secret to use to secure the data. * @param data The data to encrypt. */ @@ -14753,9 +15116,9 @@ interface Crypto { * * @description Always choose a strong unique secret. Use a password manager such as LastPass or 1Password to * help you create and keep track of them. - * + * * Keypairs are made up of a private key and a public key. - * The public key is a special value that can be used to encrypt data and + * The public key is a special value that can be used to encrypt data and * the private key is a related value that can be used to decrypt data that was encrypted by the public key. * * The private key is called "private" because it is encrypted using the given secret @@ -14786,7 +15149,7 @@ interface Crypto { * * Encrypts the given data using an asymmetric authenticated encryption mechanism * based on x25519 (A key-exchange mechanism), XSalsa20 (An encryption cipher) and Poly1305 (A message authentication code). - * + * * You may notice that this function does not need a secret to decrypt the keypair. * This is because the public key of the keypair is used to encrypt the data. * Due to how asymmetric encryption works, only the encrypted private key will be able to decrypt the data. @@ -14838,10 +15201,10 @@ interface Crypto { * Digital signatures are used to verifying the authenticity and integrity of data. * * This works by leveraging asymetric encryption but in reverse. - * + * * If we can encrypt some data such that only the public key of a keypair can decrypt it, then we can prove that * the data was encrypted (i.e. signed) by the corresponding private key. - * + * * And since the public key is available to everyone but the private * key is only usable when you have the secret, we can use this to prove that a particular piece of data was signed by whoever knows the secret. * @@ -14861,7 +15224,7 @@ interface Crypto { /** * Creates a new certified bot that is signed using the given certified bot. - * @param certificate The certified bot that the new certificate should be signed with. + * @param certificate The certified bot that the new certificate should be signed with. * This is commonly known as the signing certificate. * If given null, then the new certificate will be self-signed. * @param secret The signing certificate's secret. This is the secret that was used to create @@ -14869,7 +15232,11 @@ interface Crypto { * is the secret that was used to create the given keypair. * @param keypair The keypair that the new certificate should use. */ - createCertificate(certificate: Bot | string, secret: string, keypair: string): Promise; + createCertificate( + certificate: Bot | string, + secret: string, + keypair: string + ): Promise; /** * Signs the tag on the given bot using the given certificate and secret. @@ -14878,7 +15245,12 @@ interface Crypto { * @param bot The bot that should be signed. * @param tag The tag that should be signed. */ - signTag(certificate: Bot | string, secret: string, bot: Bot | string, tag: string): Promise; + signTag( + certificate: Bot | string, + secret: string, + bot: Bot | string, + tag: string + ): Promise; /** * Verifies that the given tag on the given bot has been signed by a certificate. @@ -14891,20 +15263,24 @@ interface Crypto { * Revokes the given certificate using the given secret. * In effect, this deletes the certificate bot from the server. * Additionally, any tags signed with the given certificate will no longer be verified. - * + * * If given a signer, then the specified certificate will be used to sign the revocation. * This lets you use a parent or grandparent certificate to remove the child. - * + * * If no signer is given, then the certificate will be used to revoke itself. - * + * * @param certificate The certificate that should be revoked. * @param secret The secret that should be used to decrypt the corresponding certificate's private key. * If given a signer, then this is the secret for the signer certificate. If no signer is given, * then this is the secret for the revoked certificate. * @param signer The certificate that should be used to revoke the aforementioned certificate. If not specified then the revocation will be self-signed. */ - revokeCertificate(certificate: Bot | string, secret: string, signer?: Bot | string): Promise; -}; + revokeCertificate( + certificate: Bot | string, + secret: string, + signer?: Bot | string + ): Promise; +} interface Experiment { /** @@ -14928,7 +15304,7 @@ interface Experiment { localPositionTween( bot: Bot | string, dimension: string, - position: { x?: number, y?: number, z?: number }, + position: { x?: number; y?: number; z?: number }, options?: TweenOptions ): LocalPositionTweenAction; @@ -14942,7 +15318,7 @@ interface Experiment { localRotationTween( bot: Bot | string, dimension: string, - rotation: { x?: number, y?: number, z?: number }, + rotation: { x?: number; y?: number; z?: number }, options?: TweenOptions ): LocalRotationTweenAction; @@ -14956,7 +15332,7 @@ interface Experiment { bot: Bot, dimension: string, anchorPoint: BotAnchorPoint - ): { x: number, y: number, z: number }; + ): { x: number; y: number; z: number }; /** * Creates a HTML file that, when loaded, will display the given bots in a static version of CasualOS. @@ -14999,19 +15375,25 @@ interface Experiment { * @param text The text that should be spoken. * @param options The options that should be used. */ - speakText(text: string, options?: { rate?: number, pitch?: number, voice?: string | SyntheticVoice }): Promise; + speakText( + text: string, + options?: { + rate?: number; + pitch?: number; + voice?: string | SyntheticVoice; + } + ): Promise; /** * Gets the list of synthetic voices that are supported by the system. * Returns a promise that resolves with the voices. */ getVoices(): Promise; -}; - +} /** * Defines a set of options that can be used when recording a loom. - * + * * @dochash types/loom * @doctitle Loom Types * @docsidebar Loom @@ -15030,10 +15412,9 @@ export interface RecordLoomOptions { recordName?: string | null; } - /** * Defines an interface that contains information for a loom video. - * + * * @dochash types/loom * @docname LoomVideo */ @@ -15091,7 +15472,7 @@ export interface LoomVideo { /** * Defines an interface that contains embed metadata for a loom video. - * + * * @dochash types/loom * @docname LoomVideoEmbedMetadata */ @@ -15135,24 +15516,24 @@ export interface LoomVideoEmbedMetadata { interface Loom { /** * Records a loom video using the given options. - * + * * Returns a promise that resolves with the video data. * Resolves with null if the video could not be recorded. - * + * * **Note:** Loom requires third-party cookies to be enabled. If third-party cookies are not enabled, then the Loom recording will not work. * * @param options The options to use for recording the video. - * + * * @example Record a loom video using the "SDK Standard". * const video = await loom.recordVideo({ * publicAppId: 'your-app-id' * }); - * + * * @example Record a loom video using the "SDK Custom". * const video = await loom.recordVideo({ * recordName: 'your-record-name', * }); - * + * * @dochash actions/loom * @doctitle Loom Actions * @docsidebar Loom @@ -15163,17 +15544,17 @@ interface Loom { /** * Displays the given loom video to the user. - * + * * Returns a promise that resolves when the video has been loaded. - * + * * @param video the loom video that should be displayed. - * + * * @example Display a loom video. * const video = await loom.recordVideo({ * publicAppId: 'your-app-id' * }); * await loom.watchVideo(video); - * + * * @dochash actions/loom * @docname loom.watchVideo * @docid loom.watchVideo-video @@ -15182,53 +15563,55 @@ interface Loom { /** * Displays the given loom video to the user. - * + * * Returns a promise that resolves when the video has been loaded. - * + * * @param sharedUrl the shared URL of the loom video that should be displayed. - * + * * @example Display a loom video by its URL. * await loom.watchVideo(videoUrl); - * + * * @dochash actions/loom * @docname loom.watchVideo * @docid loom.watchVideo-sharedUrl */ watchVideo(sharedUrl: string): Promise; - + watchVideo(sharedUrlOrVideo: string | LoomVideo): Promise; /** * Gets the embed metadata for the given loom video. - * + * * @param video the loom video that the embed metadata should be retrieved for. - * + * * @example Get the embed metadata for a loom video. * const video = await loom.recordVideo({ * publicAppId: 'your-app-id' * }); * const metadata = await loom.getVideoEmbedMetadata(video); - * + * * @dochash actions/loom * @docname loom.getVideoEmbedMetadata * @docid loom.getVideoEmbedMetadata-video */ - getVideoEmbedMetadata(video: LoomVideo): Promise + getVideoEmbedMetadata(video: LoomVideo): Promise; /** * Gets the embed metadata for the given loom video. - * + * * @param sharedUrl the shared URL of the the video that the embed metadata should be retrieved for. - * + * * @example Get the embed metadata for a loom video. * const metadata = await loom.getVideoEmbedMetadata(videoUrl); - * + * * @dochash actions/loom * @docname loom.getVideoEmbedMetadata * @docid loom.getVideoEmbedMetadata-sharedUrl */ getVideoEmbedMetadata(sharedUrl: string): Promise; - getVideoEmbedMetadata(sharedUrlOrVideo: string | LoomVideo): Promise; + getVideoEmbedMetadata( + sharedUrlOrVideo: string | LoomVideo + ): Promise; } interface Perf { @@ -15236,7 +15619,7 @@ interface Perf { * Gets the performance stats for the instance. */ getStats(): PerformanceStats; -}; +} interface Analytics { /** diff --git a/src/aux-runtime/runtime/RecordsEvents.ts b/src/aux-runtime/runtime/RecordsEvents.ts index 6843160b96..3244422068 100644 --- a/src/aux-runtime/runtime/RecordsEvents.ts +++ b/src/aux-runtime/runtime/RecordsEvents.ts @@ -5,6 +5,7 @@ import type { WebhookRecord, NotificationRecord, PushNotificationPayload, + XpController, } from '@casual-simulation/aux-records'; import type { RecordsClientActions } from '@casual-simulation/aux-records/RecordsClient'; import { @@ -2169,6 +2170,37 @@ export function listUserNotificationSubscriptions( ); } +export interface xpUserIdQuery { + /** The auth user Id of the xp user. */ + userId?: string; + /** The xp user Id of the xp user. */ + xpId?: string; +} + + +/** + * Creates an action that can be used to provide meta data on an auth users Xp (user) identity. + */ +export function getXpUserMeta( + by: xpUserIdQuery | string | undefined, + options: RecordActionOptions, + taskId: string | number +): RecordsCallProcedureAction { + if (typeof by === 'string') by = { userId: by }; + if (!by) by = {}; + return recordsCallProcedure( + { + getXpUserMeta: { + input: { + ...by, + }, + }, + }, + options, + taskId + ); +} + /** * Creates a RecordFileAction. * @param recordKey The key that should be used to access the record. diff --git a/src/aux-runtime/runtime/__snapshots__/AuxRuntime.spec.ts.snap b/src/aux-runtime/runtime/__snapshots__/AuxRuntime.spec.ts.snap index 47c0b07a4e..d667a7d1bb 100644 --- a/src/aux-runtime/runtime/__snapshots__/AuxRuntime.spec.ts.snap +++ b/src/aux-runtime/runtime/__snapshots__/AuxRuntime.spec.ts.snap @@ -97,6 +97,7 @@ Array [ "crypto", "perf", "web", + "xp", "analytics", "create", "setTimeout", @@ -203,6 +204,7 @@ Array [ "crypto", "perf", "web", + "xp", "analytics", "create", "setTimeout", @@ -384,6 +386,7 @@ Array [ "crypto", "perf", "web", + "xp", "analytics", "tagName", "___importMeta", @@ -488,6 +491,7 @@ Array [ "crypto", "perf", "web", + "xp", "analytics", "create", "setTimeout", @@ -594,6 +598,7 @@ Array [ "crypto", "perf", "web", + "xp", "analytics", "create", "setTimeout", diff --git a/src/aux-server/aux-backend/prisma/PrismaXpStore.ts b/src/aux-server/aux-backend/prisma/PrismaXpStore.ts new file mode 100644 index 0000000000..3486f5ff8d --- /dev/null +++ b/src/aux-server/aux-backend/prisma/PrismaXpStore.ts @@ -0,0 +1,170 @@ +import { + AuthUser, + XpAccount, + XpAccountEntry, + XpContract, + XpInvoice, + XpStore, + XpUser, +} from '@casual-simulation/aux-records'; +import { SuccessResult } from '@casual-simulation/aux-records/TypeUtils'; +import { + PrismaClient, + DataRecord as PrismaDataRecord, + Prisma, +} from './generated'; + +/** + * A helper function that runs the given function and returns null if an error occurs. + * @param fn The function to run. + * * Useful for database operations that may fail due to invalid input. + * * Still be wary of cases where invalid input could be malicious as this could be used to hide errors. + */ +async function noThrowNull any>( + fn: Fn, + ...args: Parameters +): Promise | null> { + try { + return await fn(...args); + } catch (error) { + console.warn(error); + return null; + } +} + +export class PrismaXpStore implements XpStore { + private _client: PrismaClient; + + constructor(client: PrismaClient) { + this._client = client; + } + + getXpAccount: (accountId: XpAccount['id']) => Promise; + getXpAccountEntry: ( + entryId: XpAccountEntry['id'] + ) => Promise; + getXpContract: (contractId: XpContract['id']) => Promise; + getXpInvoice: (invoiceId: XpInvoice['id']) => Promise; + + async getXpUserById(id: XpUser['id']) { + const pUser = await noThrowNull(this._client.xpUser.findUnique, { + where: { id }, + }); + return !pUser + ? null + : { + id: pUser.id, + accountId: pUser.accountId, + userId: pUser.userId, + requestedRate: pUser.requestedRate, + createdAtMs: pUser.createdAt.getTime(), + updatedAtMs: pUser.updatedAt.getTime(), + }; + } + + async getXpUserByAuthId(id: AuthUser['id']) { + const pUser = await noThrowNull(this._client.xpUser.findUnique, { + where: { userId: id }, + }); + return !pUser + ? null + : { + id: pUser.id, + accountId: pUser.accountId, + userId: pUser.userId, + requestedRate: pUser.requestedRate, + createdAtMs: pUser.createdAt.getTime(), + updatedAtMs: pUser.updatedAt.getTime(), + }; + } + + saveXpAccount( + associationId: T extends 'user' ? XpUser['id'] : XpContract['id'], + account: XpAccount + ): Promise { + throw new Error('Method not implemented.'); + } + + async saveXpContract(contract: XpContract, account: XpAccount | null) { + await this._client.xpContract.create({ + data: { + id: contract.id, + rate: contract.rate, + offeredWorth: contract.offeredWorth, + ...(account !== null + ? { + account: { + create: { + id: account.id, + currency: account.currency, + createdAt: new Date(account.createdAtMs), + updatedAt: new Date(account.updatedAtMs), + }, + }, + } + : {}), + issuer: { + connect: { + id: contract.issuerUserId, + }, + }, + ...(contract.holdingUserId !== null + ? { + holdingUser: { + connect: { + id: contract.holdingUserId, + }, + }, + } + : {}), + status: contract.status, + createdAt: new Date(contract.createdAtMs), + updatedAt: new Date(contract.updatedAtMs), + }, + }); + } + + async saveXpUserWithAccount(user: XpUser, account: XpAccount) { + await this._client.xpUser.create({ + data: { + id: user.id, + account: { + create: { + id: account.id, + currency: account.currency, + createdAt: new Date(account.createdAtMs), + updatedAt: new Date(account.updatedAtMs), + }, + }, + user: { + connect: { + id: user.userId, + }, + }, + requestedRate: user.requestedRate, + createdAt: new Date(user.createdAtMs), + updatedAt: new Date(user.updatedAtMs), + }, + }); + } + + async saveXpUser(id: XpUser['id'], user: XpUser) { + const upsert = await this._client.xpUser.upsert({ + where: { id }, + create: { + id, + accountId: user.accountId, + userId: user.userId, + requestedRate: user.requestedRate, + createdAt: new Date(user.createdAtMs), + updatedAt: new Date(user.updatedAtMs), + }, + update: { + accountId: user.accountId, + requestedRate: user.requestedRate, + updatedAt: new Date(user.updatedAtMs), + }, + }); + return { success: upsert ? true : false }; + } +} diff --git a/src/aux-server/aux-backend/prisma/index.ts b/src/aux-server/aux-backend/prisma/index.ts index 71ae092617..4096cb5745 100644 --- a/src/aux-server/aux-backend/prisma/index.ts +++ b/src/aux-server/aux-backend/prisma/index.ts @@ -5,3 +5,4 @@ export * from './PrismaFileRecordsLookup'; export * from './PrismaPolicyStore'; export * from './PrismaRecordsStore'; export * from './PrismaConfigurationStore'; +export * from './PrismaXpStore'; diff --git a/src/aux-server/aux-backend/schemas/auth.prisma b/src/aux-server/aux-backend/schemas/auth.prisma index c51c9ff624..9f3f337e18 100644 --- a/src/aux-server/aux-backend/schemas/auth.prisma +++ b/src/aux-server/aux-backend/schemas/auth.prisma @@ -83,6 +83,9 @@ model User { notificationSubscriptions NotificationSubscription[] sentNotifications SentPushNotification[] + xpUser XpUser? + isXpAdmin Boolean? + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -1145,4 +1148,240 @@ model StudioComIdRequest { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt -} \ No newline at end of file +} + +// Represents information about a user that has been onboarded to the xpExchange +model XpUser { + id String @id @db.Uuid + + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade, map: "XpUser_userId_fkey1") + + accountId String @db.Uuid @unique + account XpAccount @relation(fields: [accountId], references: [id], onDelete: Restrict, map: "XpUser_accountId_fkey1") + + // The rate at which the user is requested to be paid per gig. + // Should have the same currency as the account. + requestedRate Int? + + issuedEvents XpSystemEvent[] @relation() + issuedContracts XpContract[] @relation("IssuerUser") + heldContracts XpContract[] @relation("HoldingUser") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Represents an event that affects the xpExchange system +// This can be used to track inputs into the system, which will help us track down and reconcile any issues should they arise +// For example, when a user calls the xpCreateContract procedure, the system will record an event with the type "createContract" and the data being the input and related context. +model XpSystemEvent { + id String @id @db.Uuid + + // The type of the event + // Currently there is only one special type of event + // - adjustment: An XpSystemEventAdjustment + type String + + // The ID of the XpUser that sent the event + // Null if the event is not associated with a user + xpUserId String? @db.Uuid + xpUser XpUser? @relation(fields: [xpUserId], references: [id], onDelete: SetNull, map: "XpEvent_xpUserId_fkey1") + + // The data of the event + data Json + + // The time that the event occurred + time DateTime + + accountEntries XpAccountEntry[] + + // The adjustment that tracks the adjusting and adjusted events for this event + adjustment XpSystemEventAdjustment? @relation() + + // The ID of the adjustment that causes this event to be replaced + adjustingEventId String? @db.Uuid + adjustingEvent XpSystemEventAdjustment? @relation("AdjustingEvent", fields: [adjustingEventId], references: [id], onDelete: Restrict, map: "XpSystemEventAdjustment_adjustingEventId_fkey1") + + // The ID of the adjustment that causes this event to replace other events + adjusterEventId String? @db.Uuid + adjusterEvent XpSystemEventAdjustment? @relation("AdjusterEvent", fields: [adjusterEventId], references: [id], onDelete: Restrict, map: "XpSystemEventAdjustment_adjusterEventId_fkey1") + + // The time the event was created in the system + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Represents a special event that is able to make adjustments to a set of events in the system +// This can be used to correct mistakes in the system, or to make special adjustments to the system +// This works by identifying a set of events that need to be replaced and a set of events as replacements for the old events +// The system will then calculate the difference between the old events and the new events and apply the difference to the affected accounts +model XpSystemEventAdjustment { + id String @id @db.Uuid + + // The event that contains the basic information for the adjustment + event XpSystemEvent @relation(fields: [id], references: [id], onDelete: Cascade, map: "XpSystemEventAdjustment_id_fkey1") + + // The list of events that this adjustment replaces + oldEvents XpSystemEvent[] @relation("AdjustingEvent") + + // The list of modified events + newEvents XpSystemEvent[] @relation("AdjusterEvent") + + // The time the event was created in the system + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Represents an account of money that can be used to track entries and transactions +// The current balance of the account is the sum of all entries +model XpAccount { + id String @id @default(uuid()) @db.Uuid + + entries XpAccountEntry[] + + // The user that the account is for + user XpUser? + + // The contract that the account is for + contract XpContract? + + // The currency (implementations are expected to support both alphabetic and numeric representations + // e.g. "USD" | "840" ) of transactions in the account (Conforms to ISO 4217) + currency String + + // Time that the account was closed + // Fail-safe to help prevent making entries for accounts that are closed + // (i.e. accounts for closed contracts) + closedTime DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Get current balance +// SELECT balance FROM XpAccountEntry +// WHERE accountId = $accountId +// ORDER BY time DESC +// LIMIT 1; + +// Represents the addition or withdrawl of money from an account +model XpAccountEntry { + id String @id @db.Uuid + + // The ID of the account that the entry affects + // Null if the entry is not associated with an account. (this can happen for example when the entry affects an account outside the system) + // e.g. null for system entries, or entries that are associated with an account that is outside the system (adding money to a contract for example) + accountId String? @db.Uuid + account XpAccount? @relation(fields: [accountId], references: [id], onDelete: Restrict, map: "XpAccountEntry_accountId_fkey1") + + // The number of cents (viable base unit) that were added to the ledger + // positive for incoming money (deposits) + // negative for outgoing money (withdrawals) + amount Int + + // The new balance of the account after the entry was made + // This value can be used to quickly calculate the balance of the account + // without having to sum all entries. + // Be wary of using this value in non-transactional contexts, as in such cases it may not + // be in sync with the entries (causing a data state inconsistency introduced via implementation) + balance Int + + // The time that the entry was created in the real world + time DateTime + + // Entries with the same transaction ID are part of the same transaction + // makes it easy to track the flow of money between accounts + // IMPORTANT: The amounts of the entries in a transaction should sum up to 0 + // This means that valid transactions require at least 2 entries (assuming non-zero amounts) + transactionId String @db.Uuid + + // A (optional, yet advised) note for the entry + note String? + + // The ID of the event that caused the entry to be created + systemEventId String @db.Uuid + systemEvent XpSystemEvent @relation(fields: [systemEventId], references: [id], onDelete: Restrict, map: "XpAccountEntry_systemEventId_fkey1") + + // The time that the entry was created in the system + createdAt DateTime @default(now()) + + // The last time the entry was updated + updatedAt DateTime @updatedAt + + @@index([transactionId]) +} + +// A contract is a promise of work at a certain rate +model XpContract { + id String @id @db.Uuid + + // The user that is issuing the contract + // That is, the user who is paying for the work + issuerUserId String + issuer XpUser @relation(fields: [issuerUserId], references: [userId], onDelete: Restrict, map: "XpContract_issuerId_fkey1", name: "IssuerUser") + + // The user that is holding the contract (if the contract is not a draft) + // That is, the user who is doing the work and getting paid + holdingUserId String? + holdingUser XpUser? @relation(fields: [holdingUserId], references: [userId], onDelete: Restrict, map: "XpContract_holdingUserId_fkey1", name: "HoldingUser") + + // The rate at which gigs are paid in the contract + // The rate is in the smallest unit of the currency (e.g. cents for USD) + // The currency should always be the same as the account + rate Int + + // The worth of the contract in the smallest unit of the currency (all gigs) if the contract is a draft + // This is what would appear in the contract's account if the contract was opened + offeredWorth Int? + + // The description of the contract + description String? + + // The status of the contract + // "open" - the contract is open and can be invoiced + // "draft" - the contract is a draft and has not been dedicated to a user (holdingUser) + // "closed" - the contract is closed and cannot be invoiced + status String + + // The account that holds the money allocated to the contract + // The account will be instantiated when the contract is opened and a holdingUser is present + accountId String? @db.Uuid @unique + account XpAccount? @relation(fields: [accountId], references: [id], onDelete: Restrict, map: "XpContract_accountId_fkey1") + + invoices XpInvoice[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// An invoice is a request for payment for part of a contract +model XpInvoice { + id String @id @db.Uuid + + contractId String @db.Uuid + contract XpContract @relation(fields: [contractId], references: [id], onDelete: Restrict, map: "XpInvoice_contractId_fkey1") + + // The amount charged in the invoice, in the smallest unit of the currency used in the contracts account + amount Int + + // The status of the invoice + // "open" - the invoice has been created but not yet paid + // "paid" - the invoice has been paid + // "void" - the invoice has been cancelled + status String + + // The reason why the invoice was voided + // "rejected" - the invoice was rejected by the receiver + // "cancelled" - the invoice was cancelled by the issuer + voidReason String? + + // The ID of the transaction that paid the invoice + transactionId String? @db.Uuid + + // Additional information about the invoice + note String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/aux-server/aux-backend/schemas/migrations/20240724192412_add_xp_api/migration.sql b/src/aux-server/aux-backend/schemas/migrations/20240724192412_add_xp_api/migration.sql new file mode 100644 index 0000000000..028545e71c --- /dev/null +++ b/src/aux-server/aux-backend/schemas/migrations/20240724192412_add_xp_api/migration.sql @@ -0,0 +1,103 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "isXpAdmin" BOOL; + +-- CreateTable +CREATE TABLE "XpUser" ( + "id" UUID NOT NULL, + "userId" STRING NOT NULL, + "accountId" UUID NOT NULL, + "requestedRate" INT4 NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "XpUser_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "XpAccount" ( + "id" UUID NOT NULL, + "currency" STRING NOT NULL, + "closedTime" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "XpAccount_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "XpAccountEntry" ( + "id" UUID NOT NULL, + "accountId" UUID NOT NULL, + "amount" INT4 NOT NULL, + "balance" INT4 NOT NULL, + "time" TIMESTAMP(3) NOT NULL, + "transactionId" UUID NOT NULL, + "note" STRING, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "XpAccountEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "XpContract" ( + "id" UUID NOT NULL, + "issuerUserId" STRING NOT NULL, + "holdingUserId" STRING NOT NULL, + "rate" INT4 NOT NULL, + "description" STRING NOT NULL, + "status" STRING NOT NULL, + "accountId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "XpContract_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "XpInvoice" ( + "id" UUID NOT NULL, + "contractId" UUID NOT NULL, + "amount" INT4 NOT NULL, + "status" STRING NOT NULL, + "voidReason" STRING, + "transactionId" UUID, + "note" STRING NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "XpInvoice_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "XpUser_userId_key" ON "XpUser"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "XpUser_accountId_key" ON "XpUser"("accountId"); + +-- CreateIndex +CREATE INDEX "XpAccountEntry_transactionId_idx" ON "XpAccountEntry"("transactionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "XpContract_accountId_key" ON "XpContract"("accountId"); + +-- AddForeignKey +ALTER TABLE "XpUser" ADD CONSTRAINT "XpUser_userId_fkey1" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpUser" ADD CONSTRAINT "XpUser_accountId_fkey1" FOREIGN KEY ("accountId") REFERENCES "XpAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpAccountEntry" ADD CONSTRAINT "XpAccountEntry_accountId_fkey1" FOREIGN KEY ("accountId") REFERENCES "XpAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpContract" ADD CONSTRAINT "XpContract_issuerId_fkey1" FOREIGN KEY ("issuerUserId") REFERENCES "XpUser"("userId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpContract" ADD CONSTRAINT "XpContract_holdingUserId_fkey1" FOREIGN KEY ("holdingUserId") REFERENCES "XpUser"("userId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpContract" ADD CONSTRAINT "XpContract_accountId_fkey1" FOREIGN KEY ("accountId") REFERENCES "XpAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpInvoice" ADD CONSTRAINT "XpInvoice_contractId_fkey1" FOREIGN KEY ("contractId") REFERENCES "XpContract"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/aux-server/aux-backend/schemas/migrations/20240903145604_make_some_xp_api_properties_optional/migration.sql b/src/aux-server/aux-backend/schemas/migrations/20240903145604_make_some_xp_api_properties_optional/migration.sql new file mode 100644 index 0000000000..625d4bef41 --- /dev/null +++ b/src/aux-server/aux-backend/schemas/migrations/20240903145604_make_some_xp_api_properties_optional/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "XpContract" ALTER COLUMN "description" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "XpInvoice" ALTER COLUMN "note" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "XpUser" ALTER COLUMN "accountId" DROP NOT NULL; +ALTER TABLE "XpUser" ALTER COLUMN "requestedRate" DROP NOT NULL; diff --git a/src/aux-server/aux-backend/schemas/migrations/20241022183448_add_xp_audit_models/migration.sql b/src/aux-server/aux-backend/schemas/migrations/20241022183448_add_xp_audit_models/migration.sql new file mode 100644 index 0000000000..39940be32d --- /dev/null +++ b/src/aux-server/aux-backend/schemas/migrations/20241022183448_add_xp_audit_models/migration.sql @@ -0,0 +1,92 @@ +/* + Warnings: + + - Added the required column `systemEventId` to the `XpAccountEntry` table without a default value. This is not possible if the table is not empty. + - Added the required column `creationEventId` to the `XpContract` table without a default value. This is not possible if the table is not empty. + - Added the required column `creationEventId` to the `XpInvoice` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "XpAccountEntry" DROP CONSTRAINT "XpAccountEntry_accountId_fkey1"; + +-- DropForeignKey +ALTER TABLE "XpContract" DROP CONSTRAINT "XpContract_accountId_fkey1"; + +-- DropForeignKey +ALTER TABLE "XpContract" DROP CONSTRAINT "XpContract_holdingUserId_fkey1"; + +-- DropForeignKey +ALTER TABLE "XpContract" DROP CONSTRAINT "XpContract_issuerId_fkey1"; + +-- DropForeignKey +ALTER TABLE "XpInvoice" DROP CONSTRAINT "XpInvoice_contractId_fkey1"; + +-- AlterTable +ALTER TABLE "XpAccountEntry" ADD COLUMN "systemEventId" UUID NOT NULL; +ALTER TABLE "XpAccountEntry" ALTER COLUMN "accountId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "XpContract" ADD COLUMN "creationEventId" UUID NOT NULL; + +-- AlterTable +ALTER TABLE "XpInvoice" ADD COLUMN "creationEventId" UUID NOT NULL; + +-- CreateTable +CREATE TABLE "XpSystemEvent" ( + "id" UUID NOT NULL, + "type" STRING NOT NULL, + "xpUserId" UUID, + "data" JSONB NOT NULL, + "time" TIMESTAMP(3) NOT NULL, + "adjustingEventId" UUID, + "adjusterEventId" UUID, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "XpSystemEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "XpSystemEventAdjustment" ( + "id" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "XpSystemEventAdjustment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "XpSystemEvent" ADD CONSTRAINT "XpEvent_xpUserId_fkey1" FOREIGN KEY ("xpUserId") REFERENCES "XpUser"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpSystemEvent" ADD CONSTRAINT "XpSystemEventAdjustment_adjustingEventId_fkey1" FOREIGN KEY ("adjustingEventId") REFERENCES "XpSystemEventAdjustment"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpSystemEvent" ADD CONSTRAINT "XpSystemEventAdjustment_adjusterEventId_fkey1" FOREIGN KEY ("adjusterEventId") REFERENCES "XpSystemEventAdjustment"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpSystemEventAdjustment" ADD CONSTRAINT "XpSystemEventAdjustment_id_fkey1" FOREIGN KEY ("id") REFERENCES "XpSystemEvent"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpAccountEntry" ADD CONSTRAINT "XpAccountEntry_accountId_fkey1" FOREIGN KEY ("accountId") REFERENCES "XpAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpAccountEntry" ADD CONSTRAINT "XpAccountEntry_systemEventId_fkey1" FOREIGN KEY ("systemEventId") REFERENCES "XpSystemEvent"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpContract" ADD CONSTRAINT "XpContract_issuerId_fkey1" FOREIGN KEY ("issuerUserId") REFERENCES "XpUser"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpContract" ADD CONSTRAINT "XpContract_holdingUserId_fkey1" FOREIGN KEY ("holdingUserId") REFERENCES "XpUser"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpContract" ADD CONSTRAINT "XpContract_accountId_fkey1" FOREIGN KEY ("accountId") REFERENCES "XpAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpContract" ADD CONSTRAINT "XpInvoice_creationEventId_fkey1" FOREIGN KEY ("creationEventId") REFERENCES "XpSystemEvent"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpInvoice" ADD CONSTRAINT "XpInvoice_contractId_fkey1" FOREIGN KEY ("contractId") REFERENCES "XpContract"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "XpInvoice" ADD CONSTRAINT "XpInvoice_creationEventId_fkey1" FOREIGN KEY ("creationEventId") REFERENCES "XpSystemEvent"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/aux-server/aux-backend/schemas/migrations/20241030042205_make_xp_user_account_relation_non_nullable/migration.sql b/src/aux-server/aux-backend/schemas/migrations/20241030042205_make_xp_user_account_relation_non_nullable/migration.sql new file mode 100644 index 0000000000..63f87e29f9 --- /dev/null +++ b/src/aux-server/aux-backend/schemas/migrations/20241030042205_make_xp_user_account_relation_non_nullable/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `accountId` on table `XpUser` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "XpUser" ALTER COLUMN "accountId" SET NOT NULL; diff --git a/src/aux-server/aux-backend/schemas/migrations/20241109022415_make_xp_contract_draftable/migration.sql b/src/aux-server/aux-backend/schemas/migrations/20241109022415_make_xp_contract_draftable/migration.sql new file mode 100644 index 0000000000..7b391dc286 --- /dev/null +++ b/src/aux-server/aux-backend/schemas/migrations/20241109022415_make_xp_contract_draftable/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `creationEventId` on the `XpContract` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "XpContract" DROP CONSTRAINT "XpInvoice_creationEventId_fkey1"; + +-- AlterTable +ALTER TABLE "XpContract" DROP COLUMN "creationEventId"; +ALTER TABLE "XpContract" ADD COLUMN "offeredWorth" INT4; +ALTER TABLE "XpContract" ALTER COLUMN "holdingUserId" DROP NOT NULL; +ALTER TABLE "XpContract" ALTER COLUMN "accountId" DROP NOT NULL; diff --git a/src/aux-server/aux-backend/schemas/migrations/20241118064359_remove_xp_invoice_system_events/migration.sql b/src/aux-server/aux-backend/schemas/migrations/20241118064359_remove_xp_invoice_system_events/migration.sql new file mode 100644 index 0000000000..ada125329f --- /dev/null +++ b/src/aux-server/aux-backend/schemas/migrations/20241118064359_remove_xp_invoice_system_events/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the column `creationEventId` on the `XpInvoice` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "XpInvoice" DROP CONSTRAINT "XpInvoice_creationEventId_fkey1"; + +-- AlterTable +ALTER TABLE "XpInvoice" DROP COLUMN "creationEventId"; diff --git a/src/aux-server/aux-backend/shared/ServerBuilder.ts b/src/aux-server/aux-backend/shared/ServerBuilder.ts index 55f8290413..819446259b 100644 --- a/src/aux-server/aux-backend/shared/ServerBuilder.ts +++ b/src/aux-server/aux-backend/shared/ServerBuilder.ts @@ -50,6 +50,8 @@ import { NotificationRecordsController, NotificationRecordsStore, WebPushInterface, + XpStore, + XpController, } from '@casual-simulation/aux-records'; import { RekognitionModerationJobProvider, @@ -191,6 +193,7 @@ import { import { AuxConfigParameters } from '@casual-simulation/aux-vm'; import { WebPushImpl } from '../notifications/WebPushImpl'; import { PrismaNotificationRecordsStore } from 'aux-backend/prisma/PrismaNotificationRecordsStore'; +import { PrismaXpStore } from 'aux-backend/prisma/PrismaXpStore'; const automaticPlugins: ServerPlugin[] = [ ...xpApiPlugins.map((p: any) => p.default), @@ -290,6 +293,9 @@ export class ServerBuilder implements SubscriptionLike { private _pushInterface: WebPushInterface; private _notificationsController: NotificationRecordsController; + private _xpStore: XpStore; + private _xpController: XpController; + private _subscriptionConfig: SubscriptionConfiguration | null = null; private _subscriptionController: SubscriptionController; private _stripe: StripeIntegration; @@ -681,6 +687,7 @@ export class ServerBuilder implements SubscriptionLike { prismaClient, metricsStore ); + this._xpStore = new PrismaXpStore(prismaClient); const filesLookup = new PrismaFileRecordsLookup(prismaClient); return { @@ -1583,6 +1590,10 @@ export class ServerBuilder implements SubscriptionLike { throw new Error('A config store must be configured!'); } + if (!this._xpStore) { + throw new Error('An xp store must be configured!'); + } + if (!this._rateLimitController) { console.log('[ServerBuilder] Not using rate limiting.'); } @@ -1714,6 +1725,14 @@ export class ServerBuilder implements SubscriptionLike { }); } + if (this._xpStore) { + this._xpController = new XpController({ + xpStore: this._xpStore, + authController: this._authController, + authStore: this._authStore, + }); + } + const server = new RecordsServer({ allowedAccountOrigins: this._allowedAccountOrigins, allowedApiOrigins: this._allowedApiOrigins, @@ -1734,6 +1753,7 @@ export class ServerBuilder implements SubscriptionLike { websocketRateLimitController: this._websocketRateLimitController, webhooksController: this._webhooksController, notificationsController: this._notificationsController, + xpController: this._xpController, }); const buildReturn: BuildReturn = { diff --git a/src/aux-vm/managers/RecordsManager.ts b/src/aux-vm/managers/RecordsManager.ts index 7a91306068..9a2080b4ff 100644 --- a/src/aux-vm/managers/RecordsManager.ts +++ b/src/aux-vm/managers/RecordsManager.ts @@ -206,6 +206,9 @@ export class RecordsManager { 'sendNotification', 'listNotificationSubscriptions', 'listUserNotificationSubscriptions', + 'getXpUserMeta', + 'createXpContract', + 'updateXpContract', ]); /** diff --git a/tsconfig.json b/tsconfig.json index 79f7fd9e6a..02afc4fc9f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -45,7 +45,6 @@ { "path": "./src/rate-limit-redis" }, { "path": "./src/aux-websocket" }, { "path": "./src/aux-websocket-aws" }, - { "path": "./xpexchange/xp-api" }, { "path": "./extensions/casualos-casualware/casualware-api" }, { "path": "./src/casualos-cli" }, { "path": "./src/casualos-infra" } diff --git a/tsconfig.test.json b/tsconfig.test.json index a9ae2d5f77..0624ec2a15 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -41,7 +41,6 @@ { "path": "./src/js-interpreter" }, { "path": "./src/rate-limit-redis" }, { "path": "./src/aux-websocket" }, - { "path": "./src/aux-websocket-aws" }, - { "path": "./xpexchange/xp-api" } + { "path": "./src/aux-websocket-aws" } ] }