From c2f180e8e45105e13c68a695e4c60c9a71d7fd88 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Mon, 17 Feb 2025 22:29:15 +0100 Subject: [PATCH 1/2] feat: add signal api --- demo/backend/nexus.config.ts | 10 +- demo/backend/package.json | 2 + extensions/nexus-notifications/.gitignore | 2 + extensions/nexus-notifications/package.json | 22 + .../src/databases/databases.ts | 43 + .../src/events/events.created.ts | 29 + .../src/events/events.removed.ts | 18 + extensions/nexus-notifications/src/exports.ts | 28 + .../src/tasks/tasks.add.ts | 88 ++ .../src/tasks/tasks.list.ts | 48 + .../src/tasks/tasks.remove.ts | 35 + .../src/tasks/tasks.run-action.ts | 51 + extensions/nexus-notifications/tsconfig.json | 9 + extensions/nexus-signal/.gitignore | 2 + extensions/nexus-signal/package.json | 27 + extensions/nexus-signal/src/client/client.ts | 19 + .../nexus-signal/src/configs/configs.ts | 14 + .../nexus-signal/src/databases/databases.ts | 23 + extensions/nexus-signal/src/exports.ts | 30 + extensions/nexus-signal/src/generated/api.ts | 1271 +++++++++++++++++ .../src/services/services.signal.ts | 131 ++ .../src/services/services.socket.ts | 95 ++ .../src/tasks/tasks.get-contacts.ts | 40 + .../src/tasks/tasks.get-groups.ts | 42 + .../nexus-signal/src/tasks/tasks.send.ts | 42 + extensions/nexus-signal/src/types/message.ts | 67 + extensions/nexus-signal/tsconfig.json | 9 + packages/nexus/src/tasks/tasks.ts | 10 +- pnpm-lock.yaml | 127 ++ 29 files changed, 2327 insertions(+), 7 deletions(-) create mode 100644 extensions/nexus-notifications/.gitignore create mode 100644 extensions/nexus-notifications/package.json create mode 100644 extensions/nexus-notifications/src/databases/databases.ts create mode 100644 extensions/nexus-notifications/src/events/events.created.ts create mode 100644 extensions/nexus-notifications/src/events/events.removed.ts create mode 100644 extensions/nexus-notifications/src/exports.ts create mode 100644 extensions/nexus-notifications/src/tasks/tasks.add.ts create mode 100644 extensions/nexus-notifications/src/tasks/tasks.list.ts create mode 100644 extensions/nexus-notifications/src/tasks/tasks.remove.ts create mode 100644 extensions/nexus-notifications/src/tasks/tasks.run-action.ts create mode 100644 extensions/nexus-notifications/tsconfig.json create mode 100644 extensions/nexus-signal/.gitignore create mode 100644 extensions/nexus-signal/package.json create mode 100644 extensions/nexus-signal/src/client/client.ts create mode 100644 extensions/nexus-signal/src/configs/configs.ts create mode 100644 extensions/nexus-signal/src/databases/databases.ts create mode 100644 extensions/nexus-signal/src/exports.ts create mode 100644 extensions/nexus-signal/src/generated/api.ts create mode 100644 extensions/nexus-signal/src/services/services.signal.ts create mode 100644 extensions/nexus-signal/src/services/services.socket.ts create mode 100644 extensions/nexus-signal/src/tasks/tasks.get-contacts.ts create mode 100644 extensions/nexus-signal/src/tasks/tasks.get-groups.ts create mode 100644 extensions/nexus-signal/src/tasks/tasks.send.ts create mode 100644 extensions/nexus-signal/src/types/message.ts create mode 100644 extensions/nexus-signal/tsconfig.json diff --git a/demo/backend/nexus.config.ts b/demo/backend/nexus.config.ts index 89f3ce6..ab5f8e9 100644 --- a/demo/backend/nexus.config.ts +++ b/demo/backend/nexus.config.ts @@ -7,15 +7,17 @@ import { linear } from '@bitlerjs/nexus-linear'; import { homeassistant } from '@bitlerjs/nexus-homeassistant'; import { notes } from '@bitlerjs/nexus-notes'; import { timers } from '@bitlerjs/nexus-timers'; +import { signal } from '@bitlerjs/nexus-signal'; +import { notifications } from '@bitlerjs/nexus-notifications'; import { todos } from './src/extension.js'; const config = defineConfig({ oidc: process.env.OIDC_ISSUER_URL ? { - issuerUrl: process.env.OIDC_ISSUER_URL, - clientId: process.env.OIDC_CLIENT_ID, - } + issuerUrl: process.env.OIDC_ISSUER_URL, + clientId: process.env.OIDC_CLIENT_ID, + } : undefined, extensions: [ defineExtension(todos, {}), @@ -23,6 +25,8 @@ const config = defineConfig({ defineExtension(llmExtension, { defaultModel: 'openai.gpt-4o-mini', }), + defineExtension(signal, {}), + defineExtension(notifications, {}), defineExtension(linear, {}), defineExtension(openai, { kind: 'openai', diff --git a/demo/backend/package.json b/demo/backend/package.json index bf36e67..81f09c7 100644 --- a/demo/backend/package.json +++ b/demo/backend/package.json @@ -27,6 +27,8 @@ "@bitlerjs/nexus-notes": "workspace:*", "@bitlerjs/nexus-timers": "workspace:*", "@bitlerjs/nexus-calendars": "workspace:*", + "@bitlerjs/nexus-signal": "workspace:*", + "@bitlerjs/nexus-notifications": "workspace:*", "dotenv": "^16.4.7" } } diff --git a/extensions/nexus-notifications/.gitignore b/extensions/nexus-notifications/.gitignore new file mode 100644 index 0000000..3d2bc62 --- /dev/null +++ b/extensions/nexus-notifications/.gitignore @@ -0,0 +1,2 @@ +/dist/ +/node_modules/ diff --git a/extensions/nexus-notifications/package.json b/extensions/nexus-notifications/package.json new file mode 100644 index 0000000..4cd85db --- /dev/null +++ b/extensions/nexus-notifications/package.json @@ -0,0 +1,22 @@ +{ + "name": "@bitlerjs/nexus-notifications", + "version": "1.0.0", + "main": "dist/exports.js", + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc --build", + "dev": "tsc --build --watch" + }, + "devDependencies": { + "@bitlerjs/nexus-config": "workspace:*", + "typescript": "^5.7.2" + }, + "dependencies": { + "@bitlerjs/nexus": "workspace:*", + "@bitlerjs/nexus-data": "workspace:*", + "nanoid": "^5.0.9" + } +} diff --git a/extensions/nexus-notifications/src/databases/databases.ts b/extensions/nexus-notifications/src/databases/databases.ts new file mode 100644 index 0000000..363075c --- /dev/null +++ b/extensions/nexus-notifications/src/databases/databases.ts @@ -0,0 +1,43 @@ +import { createDatabase, createMigration } from '@bitlerjs/nexus-data'; + +const init = createMigration({ + name: 'init', + up: async (knex) => { + await knex.schema.createTable('notifications', (table) => { + table.string('id').primary(); + table.string('title').notNullable(); + table.string('message').notNullable(); + table.timestamp('createdAt').defaultTo(knex.fn.now()); + }); + + await knex.schema.createTable('notificationActions', (table) => { + table.string('id').primary(); + table.string('title').notNullable(); + table.string('description').nullable(); + table.string('notificationId').notNullable().references('id').inTable('notifications').onDelete('CASCADE'); + table.string('kind').notNullable(); + table.boolean('removeNotification').defaultTo(false); + table.json('data').notNullable(); + }); + + await knex.schema.createTable('notificationEntities', (table) => { + table.string('notificationId').notNullable().references('id').inTable('notifications').onDelete('CASCADE'); + table.string('kind').notNullable(); + table.string('role').nullable(); + table.string('entityId').notNullable(); + table.primary(['notificationId', 'kind', 'entityId']); + }); + }, + down: async (knex) => { + await knex.schema.dropTable('notificationEntities'); + await knex.schema.dropTable('notificationActions'); + await knex.schema.dropTable('notifications'); + }, +}); + +const dbConfig = createDatabase({ + name: 'notifications', + migrations: [init], +}); + +export { dbConfig }; diff --git a/extensions/nexus-notifications/src/events/events.created.ts b/extensions/nexus-notifications/src/events/events.created.ts new file mode 100644 index 0000000..9758a31 --- /dev/null +++ b/extensions/nexus-notifications/src/events/events.created.ts @@ -0,0 +1,29 @@ +import { createEvent, Type } from '@bitlerjs/nexus'; + +const notificationCreatedEvent = createEvent({ + kind: 'notification.created', + name: 'Notification created', + description: 'A notification was created', + input: Type.Object({}), + output: Type.Object({ + id: Type.String(), + title: Type.String(), + message: Type.String(), + entities: Type.Array( + Type.Object({ + kind: Type.String(), + id: Type.String(), + role: Type.Optional(Type.String()), + }), + ), + actions: Type.Array( + Type.Object({ + id: Type.String(), + title: Type.String(), + description: Type.Optional(Type.String()), + }), + ), + }), +}); + +export { notificationCreatedEvent }; diff --git a/extensions/nexus-notifications/src/events/events.removed.ts b/extensions/nexus-notifications/src/events/events.removed.ts new file mode 100644 index 0000000..1be5398 --- /dev/null +++ b/extensions/nexus-notifications/src/events/events.removed.ts @@ -0,0 +1,18 @@ +import { createEvent, Type } from '@bitlerjs/nexus'; + +const notificationRemovedEvent = createEvent({ + kind: 'notification.removed', + name: 'Notification Removed', + description: 'A notification was removed', + input: Type.Object({ + ids: Type.Optional(Type.Array(Type.String())), + }), + output: Type.Object({ + id: Type.String(), + }), + filter: async ({ input, event }) => { + return input.ids?.includes(event.id) || false; + }, +}); + +export { notificationRemovedEvent }; diff --git a/extensions/nexus-notifications/src/exports.ts b/extensions/nexus-notifications/src/exports.ts new file mode 100644 index 0000000..794e240 --- /dev/null +++ b/extensions/nexus-notifications/src/exports.ts @@ -0,0 +1,28 @@ +import { createExtension, EventsService, TasksService } from '@bitlerjs/nexus'; + +import { notificationRemovedEvent } from './events/events.removed.js'; +import { notificationCreatedEvent } from './events/events.created.js'; +import { addNotificationTask } from './tasks/tasks.add.js'; +import { listNotificationTask } from './tasks/tasks.list.js'; +import { removeNotificationsTask } from './tasks/tasks.remove.js'; +import { runNotificationActionTask } from './tasks/tasks.run-action.js'; + +const tasks = { + addNotification: addNotificationTask, + listNotifications: listNotificationTask, + removeNotifications: removeNotificationsTask, + runNotificationAction: runNotificationActionTask, +}; + +const events = [notificationRemovedEvent, notificationCreatedEvent]; +const notifications = createExtension({ + setup: async ({ container }) => { + const tasksService = container.get(TasksService); + tasksService.register(Object.values(tasks)); + + const eventsService = container.get(EventsService); + eventsService.register(Object.values(events)); + }, +}); + +export { notifications, tasks, events }; diff --git a/extensions/nexus-notifications/src/tasks/tasks.add.ts b/extensions/nexus-notifications/src/tasks/tasks.add.ts new file mode 100644 index 0000000..f59aa7b --- /dev/null +++ b/extensions/nexus-notifications/src/tasks/tasks.add.ts @@ -0,0 +1,88 @@ +import { EventsService, Type, createTask } from '@bitlerjs/nexus'; +import { nanoid } from 'nanoid'; +import { Databases } from '@bitlerjs/nexus-data'; + +import { dbConfig } from '../databases/databases.js'; +import { notificationCreatedEvent } from '../events/events.created.js'; + +const addNotificationTask = createTask({ + kind: 'notification.add', + name: 'Add notification', + description: 'Add a notification', + input: Type.Object({ + id: Type.Optional(Type.String()), + title: Type.String(), + message: Type.String(), + entities: Type.Optional( + Type.Array( + Type.Object({ + kind: Type.String(), + id: Type.String(), + rolw: Type.Optional(Type.String()), + }), + ), + ), + actions: Type.Optional( + Type.Array( + Type.Object({ + title: Type.String(), + description: Type.Optional(Type.String()), + kind: Type.String(), + removeNotification: Type.Optional(Type.Boolean()), + data: Type.Unknown(), + }), + ), + ), + }), + output: Type.Object({ + id: Type.String(), + }), + handler: async ({ input, container }) => { + const dbs = container.get(Databases); + const eventsService = container.get(EventsService); + const db = await dbs.get(dbConfig); + const id = input.id || nanoid(); + const actions = input.actions?.map((action) => ({ + id, + title: action.title, + description: action.description, + kind: action.kind, + removeNotification: action.removeNotification || false, + data: action.data, + })); + + await db.transaction(async (trx) => { + await trx('notifications') + .insert({ + id, + title: input.title, + message: input.message, + }) + .onConflict('id') + .merge(); + + await trx('notificationActions').where('notificationId', id).delete(); + await trx('notificationEntities').where('notificationId', id).delete(); + + if (actions) { + await trx('notificationActions').insert(actions); + } + + if (input.entities) { + await trx('notificationEntities').insert(input.entities.map((entity) => ({ ...entity, notificationId: id }))); + } + }); + + eventsService.emit(notificationCreatedEvent, { + id, + title: input.title, + message: input.message, + actions: actions || [], + entities: input.entities || [], + }); + + return { id }; + }, +}); + +export { addNotificationTask }; diff --git a/extensions/nexus-notifications/src/tasks/tasks.list.ts b/extensions/nexus-notifications/src/tasks/tasks.list.ts new file mode 100644 index 0000000..27ab39c --- /dev/null +++ b/extensions/nexus-notifications/src/tasks/tasks.list.ts @@ -0,0 +1,48 @@ +import { createTask, Type } from '@bitlerjs/nexus'; +import { Databases } from '@bitlerjs/nexus-data'; + +import { dbConfig } from '../databases/databases.js'; + +const listNotificationTask = createTask({ + kind: 'notification.list', + name: 'List', + group: 'Notification', + description: 'List notifications', + input: Type.Object({}), + output: Type.Object({ + notifications: Type.Array( + Type.Object({ + id: Type.String(), + title: Type.String(), + message: Type.String(), + createdAt: Type.String(), + actions: Type.Array( + Type.Object({ + id: Type.String(), + title: Type.String(), + description: Type.Optional(Type.String()), + kind: Type.String(), + removeNotification: Type.Optional(Type.Boolean()), + data: Type.Unknown(), + }), + ), + }), + ), + }), + handler: async ({ container }) => { + const dbs = container.get(Databases); + const db = await dbs.get(dbConfig); + + const notifications = await db('notifications').select('*'); + const actions = await db('notificationActions').select('*'); + + const result = notifications.map((notification) => ({ + ...notification, + actions: actions.filter((action) => action.notificationId === notification.id), + })); + + return { notifications: result }; + }, +}); + +export { listNotificationTask }; diff --git a/extensions/nexus-notifications/src/tasks/tasks.remove.ts b/extensions/nexus-notifications/src/tasks/tasks.remove.ts new file mode 100644 index 0000000..46313d5 --- /dev/null +++ b/extensions/nexus-notifications/src/tasks/tasks.remove.ts @@ -0,0 +1,35 @@ +import { createTask, EventsService, Type } from '@bitlerjs/nexus'; +import { Databases } from '@bitlerjs/nexus-data'; + +import { dbConfig } from '../databases/databases.js'; +import { notificationRemovedEvent } from '../events/events.removed.js'; + +const removeNotificationsTask = createTask({ + kind: 'notification.remove', + name: 'Remove', + group: 'Notification', + description: 'Remove a notification', + input: Type.Object({ + ids: Type.Array(Type.String()), + }), + output: Type.Object({ + success: Type.Boolean(), + }), + handler: async ({ input, container }) => { + const dbs = container.get(Databases); + const eventsService = container.get(EventsService); + const db = await dbs.get(dbConfig); + + await db.transaction(async (trx) => { + await trx('notifications').whereIn('id', input.ids).delete(); + }); + + input.ids.forEach((id) => { + eventsService.emit(notificationRemovedEvent, { id }); + }); + + return { success: true }; + }, +}); + +export { removeNotificationsTask }; diff --git a/extensions/nexus-notifications/src/tasks/tasks.run-action.ts b/extensions/nexus-notifications/src/tasks/tasks.run-action.ts new file mode 100644 index 0000000..f67f1d3 --- /dev/null +++ b/extensions/nexus-notifications/src/tasks/tasks.run-action.ts @@ -0,0 +1,51 @@ +import { createTask, TasksService, Type } from '@bitlerjs/nexus'; +import { Databases } from '@bitlerjs/nexus-data'; + +import { dbConfig } from '../databases/databases.js'; + +const runNotificationActionTask = createTask({ + kind: 'notification.run-action', + name: 'Run Action', + group: 'Notification', + description: 'Run a notification action', + input: Type.Object({ + actionId: Type.String(), + }), + output: Type.Object({ + actionId: Type.String(), + notificationId: Type.String(), + success: Type.Boolean(), + }), + handler: async ({ input, container, requestContext, continuation }) => { + const dbs = container.get(Databases); + const db = await dbs.get(dbConfig); + + const [action] = await db('notificationActions').select('*').where('id', input.actionId).limit(1); + if (!action) { + throw new Error('Action not found'); + } + const tasksService = container.get(TasksService); + const task = tasksService.get(action.kind); + if (!task) { + throw new Error('Task not found'); + } + await tasksService.run({ + task, + input: action.data, + requestContext, + continuation, + }); + + if (action.removeNotification) { + await db('notifications').where('id', action.notificationId).delete(); + } + + return { + success: true, + actionId: action.id, + notificationId: action.notificationId, + }; + }, +}); + +export { runNotificationActionTask }; diff --git a/extensions/nexus-notifications/tsconfig.json b/extensions/nexus-notifications/tsconfig.json new file mode 100644 index 0000000..fadd53c --- /dev/null +++ b/extensions/nexus-notifications/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@bitlerjs/nexus-config", + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "src/**/*" + ] +} diff --git a/extensions/nexus-signal/.gitignore b/extensions/nexus-signal/.gitignore new file mode 100644 index 0000000..3d2bc62 --- /dev/null +++ b/extensions/nexus-signal/.gitignore @@ -0,0 +1,2 @@ +/dist/ +/node_modules/ diff --git a/extensions/nexus-signal/package.json b/extensions/nexus-signal/package.json new file mode 100644 index 0000000..67d66e3 --- /dev/null +++ b/extensions/nexus-signal/package.json @@ -0,0 +1,27 @@ +{ + "name": "@bitlerjs/nexus-signal", + "version": "1.0.0", + "main": "dist/exports.js", + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc --build", + "dev": "tsc --build --watch", + "generate:types": "openapi-typescript https://bbernhard.github.io/signal-cli-rest-api/src/docs/swagger.json -o src/generated/api.ts" + }, + "devDependencies": { + "@bitlerjs/nexus-config": "workspace:*", + "openapi-typescript": "^5.4.2", + "typescript": "^5.7.2" + }, + "dependencies": { + "@bitlerjs/nexus": "workspace:*", + "@bitlerjs/nexus-configs": "workspace:*", + "@bitlerjs/nexus-data": "workspace:*", + "@bitlerjs/nexus-notifications": "workspace:*", + "nanoid": "^5.0.9", + "openapi-fetch": "^0.13.3" + } +} diff --git a/extensions/nexus-signal/src/client/client.ts b/extensions/nexus-signal/src/client/client.ts new file mode 100644 index 0000000..8e18bdf --- /dev/null +++ b/extensions/nexus-signal/src/client/client.ts @@ -0,0 +1,19 @@ +import createBaseClient from 'openapi-fetch'; + +import { paths } from '../generated/api.js'; + +type CreateClientOptions = { + host: string; + secure: boolean; +}; +const createClient = ({ host, secure }: CreateClientOptions) => { + const client = createBaseClient({ + baseUrl: `${secure ? 'https' : 'http'}://${host}`, + }); + + return client; +}; + +type Client = ReturnType; + +export { createClient, type Client }; diff --git a/extensions/nexus-signal/src/configs/configs.ts b/extensions/nexus-signal/src/configs/configs.ts new file mode 100644 index 0000000..bf7a444 --- /dev/null +++ b/extensions/nexus-signal/src/configs/configs.ts @@ -0,0 +1,14 @@ +import { Type } from '@bitlerjs/nexus'; +import { createConfig } from '@bitlerjs/nexus-configs'; + +const signalConfig = createConfig({ + kind: 'signal.integration', + name: 'Signal', + group: 'Integrations', + description: 'Signal integration', + schema: Type.Object({ + apiUrl: Type.String(), + }), +}); + +export { signalConfig }; diff --git a/extensions/nexus-signal/src/databases/databases.ts b/extensions/nexus-signal/src/databases/databases.ts new file mode 100644 index 0000000..ba55508 --- /dev/null +++ b/extensions/nexus-signal/src/databases/databases.ts @@ -0,0 +1,23 @@ +import { createDatabase, createMigration } from '@bitlerjs/nexus-data'; + +const addNotificationsTable = createMigration({ + name: 'add-notifications-table', + up: async (knex) => { + await knex.schema.createTable('notifications', (table) => { + table.increments('id'); + table.string('senderUuid'); + table.string('notificationId'); + table.datetime('timestamp').defaultTo(knex.fn.now()); + }); + }, + down: async (knex) => { + await knex.schema.dropTable('notifications'); + }, +}); + +const dbConfig = createDatabase({ + name: 'signal', + migrations: [addNotificationsTable], +}); + +export { dbConfig }; diff --git a/extensions/nexus-signal/src/exports.ts b/extensions/nexus-signal/src/exports.ts new file mode 100644 index 0000000..1ed15f9 --- /dev/null +++ b/extensions/nexus-signal/src/exports.ts @@ -0,0 +1,30 @@ +import { createExtension, TasksService } from '@bitlerjs/nexus'; +import { ConfigService } from '@bitlerjs/nexus-configs'; + +import { SignalService } from './services/services.signal.js'; +import { signalConfig } from './configs/configs.js'; +import { getContactsTask } from './tasks/tasks.get-contacts.js'; +import { getGroupsTask } from './tasks/tasks.get-groups.js'; +import { sendTask } from './tasks/tasks.send.js'; + +const signal = createExtension({ + setup: async ({ container }) => { + const configsService = container.get(ConfigService); + const tasksService = container.get(TasksService); + + configsService.register([signalConfig]); + + configsService.use({ + config: signalConfig, + handler: async (config) => { + if (config) { + const signalService = container.get(SignalService); + await signalService.setup(); + tasksService.register([getContactsTask, getGroupsTask, sendTask]); + } + }, + }); + }, +}); + +export { signal }; diff --git a/extensions/nexus-signal/src/generated/api.ts b/extensions/nexus-signal/src/generated/api.ts new file mode 100644 index 0000000..975ba7f --- /dev/null +++ b/extensions/nexus-signal/src/generated/api.ts @@ -0,0 +1,1271 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export type paths = { + '/v1/about': { + /** Returns the supported API versions and the internal build nr */ + get: { + responses: { + /** OK */ + 200: { + schema: definitions['client.About']; + }; + }; + }; + }; + '/v1/accounts': { + /** Lists all of the accounts linked or registered */ + get: { + responses: { + /** OK */ + 200: { + schema: string[]; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/accounts/{number}/rate-limit-challenge': { + /** When running into rate limits, sometimes the limit can be lifted, by solving a CAPTCHA. To get the captcha token, go to https://signalcaptchas.org/challenge/generate.html For the staging environment, use: https://signalcaptchas.org/staging/registration/generate.html. The "challenge_token" is the token from the failed send attempt. The "captcha" is the captcha result, starting with signalcaptcha:// */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Request */ + data: definitions['api.RateLimitChallengeRequest']; + }; + }; + responses: { + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/accounts/{number}/settings': { + /** Update the account attributes on the signal server. */ + put: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Request */ + data: definitions['api.UpdateAccountSettingsRequest']; + }; + }; + responses: { + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/accounts/{number}/username': { + /** Allows to set the username that should be used for this account. This can either be just the nickname (e.g. test) or the complete username with discriminator (e.g. test.123). Returns the new username with discriminator and the username link. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Request */ + data: definitions['api.SetUsernameRequest']; + }; + }; + responses: { + /** Created */ + 201: { + schema: definitions['client.SetUsernameResponse']; + }; + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Delete the username associated with this account. */ + delete: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + }; + responses: { + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/attachments': { + /** List all downloaded attachments */ + get: { + responses: { + /** OK */ + 200: { + schema: string[]; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/attachments/{attachment}': { + /** Serve the attachment with the given id */ + get: { + parameters: { + path: { + /** Attachment ID */ + attachment: string; + }; + }; + responses: { + /** OK */ + 200: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Remove the attachment with the given id from filesystem. */ + delete: { + parameters: { + path: { + /** Attachment ID */ + attachment: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/configuration': { + /** List the REST API configuration. */ + get: { + responses: { + /** OK */ + 200: { + schema: definitions['api.Configuration']; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Set the REST API configuration. */ + post: { + parameters: { + body: { + /** Configuration */ + data: definitions['api.Configuration']; + }; + }; + responses: { + /** OK */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/configuration/{number}/settings': { + /** List account specific settings. */ + get: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Request */ + data: definitions['api.TrustModeResponse']; + }; + }; + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Set account specific settings. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Request */ + data: definitions['api.TrustModeRequest']; + }; + }; + responses: { + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/contacts/{number}': { + /** List all contacts for the given number. */ + get: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + }; + responses: { + /** OK */ + 200: { + schema: definitions['client.ListContactsResponse'][]; + }; + }; + }; + /** Updates the info associated to a number on the contact list. */ + put: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Contact */ + data: definitions['api.UpdateContactRequest']; + }; + }; + responses: { + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/contacts/{number}/sync': { + /** Send a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the primary device. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + }; + responses: { + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/devices/{number}': { + /** Links another device to this device. Only works, if this is the master device. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Request */ + data: definitions['api.AddDeviceRequest']; + }; + }; + responses: { + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/groups/{number}': { + /** List all Signal Groups. */ + get: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + }; + responses: { + /** OK */ + 200: { + schema: definitions['client.GroupEntry'][]; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Create a new Signal Group with the specified members. */ + post: { + parameters: { + body: { + /** Input Data */ + data: definitions['api.CreateGroupRequest']; + }; + path: { + /** Registered Phone Number */ + number: string; + }; + }; + responses: { + /** Created */ + 201: { + schema: definitions['api.CreateGroupResponse']; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/groups/{number}/{groupid}': { + /** List a specific Signal Group. */ + get: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + /** Group ID */ + groupid: string; + }; + }; + responses: { + /** OK */ + 200: { + schema: definitions['client.GroupEntry']; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Update the state of a Signal Group. */ + put: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + /** Group ID */ + groupid: string; + }; + body: { + /** Input Data */ + data: definitions['api.UpdateGroupRequest']; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Delete the specified Signal Group. */ + delete: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + /** Group Id */ + groupid: string; + }; + }; + responses: { + /** OK */ + 200: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/groups/{number}/{groupid}/admins': { + /** Add one or more admins to an existing Signal Group. */ + post: { + parameters: { + body: { + /** Admins */ + data: definitions['api.ChangeGroupAdminsRequest']; + }; + path: { + /** Registered Phone Number */ + number: string; + /** Group ID */ + groupid: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Remove one or more admins from an existing Signal Group. */ + delete: { + parameters: { + body: { + /** Admins */ + data: definitions['api.ChangeGroupAdminsRequest']; + }; + path: { + /** Registered Phone Number */ + number: string; + /** Group ID */ + groupid: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/groups/{number}/{groupid}/block': { + /** Block the specified Signal Group. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + /** Group ID */ + groupid: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/groups/{number}/{groupid}/join': { + /** Join the specified Signal Group. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + /** Group ID */ + groupid: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/groups/{number}/{groupid}/members': { + /** Add one or more members to an existing Signal Group. */ + post: { + parameters: { + body: { + /** Members */ + data: definitions['api.ChangeGroupMembersRequest']; + }; + path: { + /** Registered Phone Number */ + number: string; + /** Group ID */ + groupid: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Remove one or more members from an existing Signal Group. */ + delete: { + parameters: { + body: { + /** Members */ + data: definitions['api.ChangeGroupMembersRequest']; + }; + path: { + /** Registered Phone Number */ + number: string; + /** Group ID */ + groupid: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/groups/{number}/{groupid}/quit': { + /** Quit the specified Signal Group. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + /** Group ID */ + groupid: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/health': { + /** Internally used by the docker container to perform the health check. */ + get: { + responses: { + /** No Content */ + 204: { + schema: string; + }; + }; + }; + }; + '/v1/identities/{number}': { + /** List all identities for the given number. */ + get: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + }; + responses: { + /** OK */ + 200: { + schema: definitions['client.IdentityEntry'][]; + }; + }; + }; + }; + '/v1/identities/{number}/trust/{numberToTrust}': { + /** Trust an identity. When 'trust_all_known_keys' is set to' true', all known keys of this user are trusted. **This is only recommended for testing.** */ + put: { + parameters: { + body: { + /** Input Data */ + data: definitions['api.TrustIdentityRequest']; + }; + path: { + /** Registered Phone Number */ + number: string; + /** Number To Trust */ + numberToTrust: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + }; + }; + }; + '/v1/profiles/{number}': { + /** Set your name and optional an avatar. */ + put: { + parameters: { + body: { + /** Profile Data */ + data: definitions['api.UpdateProfileRequest']; + }; + path: { + /** Registered Phone Number */ + number: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/qrcodelink': { + /** Link device and generate QR code */ + get: { + parameters: { + query: { + /** Device Name */ + device_name: string; + /** QRCode Version (defaults to 10) */ + qrcode_version?: number; + }; + }; + responses: { + /** Image */ + 200: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/reactions/{number}': { + /** React to a message */ + post: { + parameters: { + body: { + /** Reaction */ + data: definitions['api.Reaction']; + }; + path: { + /** Registered phone number */ + number: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Remove a reaction */ + delete: { + parameters: { + body: { + /** Reaction */ + data: definitions['api.Reaction']; + }; + path: { + /** Registered phone number */ + number: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/receipts/{number}': { + /** Send a read or viewed receipt */ + post: { + parameters: { + body: { + /** Receipt */ + data: definitions['api.Receipt']; + }; + path: { + /** Registered phone number */ + number: string; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/receive/{number}': { + /** Receives Signal Messages from the Signal Network. If you are running the docker container in normal/native mode, this is a GET endpoint. In json-rpc mode this is a websocket endpoint. */ + get: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + query: { + /** Receive timeout in seconds (default: 1) */ + timeout?: string; + /** Specify whether the attachments of the received message should be ignored */ + ignore_attachments?: string; + /** Specify whether stories should be ignored when receiving messages */ + ignore_stories?: string; + /** Specify the maximum number of messages to receive (default: unlimited) */ + max_messages?: string; + /** Specify whether read receipts should be sent when receiving messages */ + send_read_receipts?: string; + }; + }; + responses: { + /** OK */ + 200: { + schema: string[]; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/register/{number}': { + /** Register a phone number with the signal network. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Additional Settings */ + data?: definitions['api.RegisterNumberRequest']; + }; + }; + responses: { + /** Created */ + 201: unknown; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/register/{number}/verify/{token}': { + /** Verify a registered phone number with the signal network. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + /** Verification Code */ + token: string; + }; + body: { + /** Additional Settings */ + data?: definitions['api.VerifyNumberSettings']; + }; + }; + responses: { + /** OK */ + 201: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/search/{number}': { + /** Check if one or more phone numbers are registered with the Signal Service. */ + get: { + parameters: { + path: { + /** Registered Phone Number */ + number?: string; + }; + query: { + /** Numbers to check */ + numbers: string[]; + }; + }; + responses: { + /** OK */ + 200: { + schema: definitions['api.SearchResponse'][]; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/send': { + /** Send a signal message */ + post: { + parameters: { + body: { + /** Input Data */ + data: definitions['api.SendMessageV1']; + }; + }; + responses: { + /** OK */ + 201: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/sticker-packs/{number}': { + /** List Installed Sticker Packs. */ + get: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + }; + responses: { + /** OK */ + 200: { + schema: definitions['client.ListInstalledStickerPacksResponse'][]; + }; + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** In order to add a sticker pack, browse to https://signalstickers.org/ and select the sticker pack you want to add. Then, press the "Add to Signal" button. If you look at the address bar in your browser you should see an URL in this format: https://signal.art/addstickers/#pack_id=XXX&pack_key=YYY, where XXX is the pack_id and YYY is the pack_key. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Request */ + data: definitions['api.AddStickerPackRequest']; + }; + }; + responses: { + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/typing-indicator/{number}': { + /** Show Typing Indicator. */ + put: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Type */ + data: definitions['api.TypingIndicatorRequest']; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + /** Hide Typing Indicator. */ + delete: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Type */ + data: definitions['api.TypingIndicatorRequest']; + }; + }; + responses: { + /** No Content */ + 204: { + schema: string; + }; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v1/unregister/{number}': { + /** Disables push support for this device. **WARNING:** If *delete_account* is set to *true*, the account will be deleted from the Signal Server. This cannot be undone without loss. */ + post: { + parameters: { + path: { + /** Registered Phone Number */ + number: string; + }; + body: { + /** Additional Settings */ + data?: definitions['api.UnregisterNumberRequest']; + }; + }; + responses: { + /** No Content */ + 204: never; + /** Bad Request */ + 400: { + schema: definitions['api.Error']; + }; + }; + }; + }; + '/v2/send': { + /** Send a signal message. Set the text_mode to 'styled' in case you want to add formatting to your text message. Styling Options: *italic text*, **bold text**, ~strikethrough text~. If you want to escape a formatting character, prefix it with two backslashes ('\\') */ + post: { + parameters: { + body: { + /** Input Data */ + data: definitions['api.SendMessageV2']; + }; + }; + responses: { + /** Created */ + 201: { + schema: definitions['api.SendMessageResponse']; + }; + /** Bad Request */ + 400: { + schema: definitions['api.SendMessageError']; + }; + }; + }; + }; +}; + +export type definitions = { + 'api.AddDeviceRequest': { + uri?: string; + }; + 'api.AddStickerPackRequest': { + /** @example 9a32eda01a7a28574f2eb48668ae0dc4 */ + pack_id?: string; + /** @example 19546e18eba0ff69dea78eb591465289d39e16f35e58389ae779d4f9455aff3a */ + pack_key?: string; + }; + 'api.ChangeGroupAdminsRequest': { + admins?: string[]; + }; + 'api.ChangeGroupMembersRequest': { + members?: string[]; + }; + 'api.Configuration': { + logging?: definitions['api.LoggingConfiguration']; + }; + 'api.CreateGroupRequest': { + description?: string; + expiration_time?: number; + /** @enum {string} */ + group_link?: 'disabled' | 'enabled' | 'enabled-with-approval'; + members?: string[]; + name?: string; + permissions?: definitions['api.GroupPermissions']; + }; + 'api.CreateGroupResponse': { + id?: string; + }; + 'api.Error': { + error?: string; + }; + 'api.GroupPermissions': { + /** @enum {string} */ + add_members?: 'only-admins' | 'every-member'; + /** @enum {string} */ + edit_group?: 'only-admins' | 'every-member'; + }; + 'api.LoggingConfiguration': { + Level?: string; + }; + 'api.RateLimitChallengeRequest': { + /** @example signalcaptcha://{captcha value} */ + captcha?: string; + /** @example */ + challenge_token?: string; + }; + 'api.Reaction': { + reaction?: string; + recipient?: string; + target_author?: string; + timestamp?: number; + }; + 'api.Receipt': { + /** @enum {string} */ + receipt_type?: 'read' | 'viewed'; + recipient?: string; + timestamp?: number; + }; + 'api.RegisterNumberRequest': { + captcha?: string; + use_voice?: boolean; + }; + 'api.SearchResponse': { + number?: string; + registered?: boolean; + }; + 'api.SendMessageError': { + account?: string; + challenge_tokens?: string[]; + error?: string; + }; + 'api.SendMessageResponse': { + timestamp?: string; + }; + 'api.SendMessageV1': { + /** @example '' OR 'data:;base64,' OR 'data:;filename=;base64,' */ + base64_attachment?: string; + is_group?: boolean; + message?: string; + number?: string; + recipients?: string[]; + }; + 'api.SendMessageV2': { + /** + * @example [ + * "", + * "data:;base64", + * "data:;filename=;base64" + * ] + */ + base64_attachments?: string[]; + edit_timestamp?: number; + mentions?: definitions['data.MessageMention'][]; + message?: string; + notify_self?: boolean; + number?: string; + quote_author?: string; + quote_mentions?: definitions['data.MessageMention'][]; + quote_message?: string; + quote_timestamp?: number; + recipients?: string[]; + sticker?: string; + /** @enum {string} */ + text_mode?: 'normal' | 'styled'; + }; + 'api.SetUsernameRequest': { + /** @example test */ + username?: string; + }; + 'api.TrustIdentityRequest': { + /** @example false */ + trust_all_known_keys?: boolean; + verified_safety_number?: string; + }; + 'api.TrustModeRequest': { + trust_mode?: string; + }; + 'api.TrustModeResponse': { + trust_mode?: string; + }; + 'api.TypingIndicatorRequest': { + recipient?: string; + }; + 'api.UnregisterNumberRequest': { + /** @example false */ + delete_account?: boolean; + /** @example false */ + delete_local_data?: boolean; + }; + 'api.UpdateAccountSettingsRequest': { + discoverable_by_number?: boolean; + share_number?: boolean; + }; + 'api.UpdateContactRequest': { + expiration_in_seconds?: number; + name?: string; + recipient?: string; + }; + 'api.UpdateGroupRequest': { + base64_avatar?: string; + description?: string; + expiration_time?: number; + name?: string; + }; + 'api.UpdateProfileRequest': { + about?: string; + base64_avatar?: string; + name?: string; + }; + 'api.VerifyNumberSettings': { + pin?: string; + }; + 'client.About': { + build?: number; + capabilities?: Record; + mode?: string; + version?: string; + versions?: string[]; + }; + 'client.GroupEntry': { + admins?: string[]; + blocked?: boolean; + id?: string; + internal_id?: string; + invite_link?: string; + members?: string[]; + name?: string; + pending_invites?: string[]; + pending_requests?: string[]; + }; + 'client.IdentityEntry': { + added?: string; + fingerprint?: string; + number?: string; + safety_number?: string; + status?: string; + }; + 'client.ListContactsResponse': { + blocked?: boolean; + color?: string; + message_expiration?: string; + name?: string; + number?: string; + profile_name?: string; + username?: string; + uuid?: string; + }; + 'client.ListInstalledStickerPacksResponse': { + author?: string; + installed?: boolean; + pack_id?: string; + title?: string; + url?: string; + }; + 'client.SetUsernameResponse': { + username?: string; + username_link?: string; + }; + 'data.MessageMention': { + author?: string; + length?: number; + start?: number; + }; +}; + +export type operations = {}; + +export type external = {}; diff --git a/extensions/nexus-signal/src/services/services.signal.ts b/extensions/nexus-signal/src/services/services.signal.ts new file mode 100644 index 0000000..3334a7e --- /dev/null +++ b/extensions/nexus-signal/src/services/services.signal.ts @@ -0,0 +1,131 @@ +import { Container } from '@bitlerjs/nexus'; + +import { Client, createClient } from '../client/client.js'; +import { paths } from '../generated/api.js'; + +import { SignalSocket } from './services.socket.js'; + +type ApiReponse = TMethod extends keyof paths[TPath] + ? paths[TPath][TMethod] extends { responses: { 200: { schema: infer U } } } + ? U + : never + : never; + +type ApiBody = TMethod extends keyof paths[TPath] + ? paths[TPath][TMethod] extends { parameters: { body: { data: infer U } } } + ? U + : never + : never; + +type ApiPathParamters = TMethod extends keyof paths[TPath] + ? paths[TPath][TMethod] extends { parameters: { path: infer U } } + ? U + : never + : never; + +type ApiQueryParameters = TMethod extends keyof paths[TPath] + ? paths[TPath][TMethod] extends { parameters: { query: infer U } } + ? U + : never + : never; + +// TODO: FIX +const host = 'localhost'; +const secure = false; + +class SignalService { + #client: Client; + #container: Container; + #setupPromise: Promise | null = null; + + constructor(container: Container) { + this.#container = container; + this.#client = createClient({ + host, + secure, + }); + } + + #setup = async () => { + const accounts = await this.get('/v1/accounts'); + if (!accounts) { + throw new Error('No accounts found'); + } + const sockets = accounts.map( + (account) => + new SignalSocket({ + id: account, + host, + secure, + container: this.#container, + }), + ); + + return sockets; + }; + + public setup = async () => { + if (!this.#setupPromise) { + this.#setupPromise = this.#setup(); + } + return this.#setupPromise; + }; + + public destroy = async () => { + if (!this.#setupPromise) { + return; + } + + const sockets = await this.#setupPromise; + sockets.forEach((socket) => socket.destroy()); + }; + + public getAccounts = async () => { + const accounts = await this.setup(); + return accounts.map((account) => account.id); + }; + + public get = async ( + path: TPath, + params: { + query?: ApiQueryParameters; + path?: ApiPathParamters; + } = {}, + ): Promise> => { + const { data, error } = await this.#client.GET(path as any, { + params: { + query: params.query, + path: params.path, + }, + }); + if (error) { + console.error(error); + throw new Error('API error'); + } + return data! as ApiReponse; + }; + + public post = async ( + path: TPath, + params: { + body: ApiBody; + query?: ApiQueryParameters; + path?: ApiPathParamters; + }, + ): Promise> => { + const { data, error } = await this.#client.POST(path as any, { + body: params.body, + params: { + query: params.query, + path: params.path, + }, + }); + if (error) { + console.error(error); + throw new Error('API error'); + } + return data! as ApiReponse; + }; +} + +export { SignalService }; diff --git a/extensions/nexus-signal/src/services/services.socket.ts b/extensions/nexus-signal/src/services/services.socket.ts new file mode 100644 index 0000000..0ec6afe --- /dev/null +++ b/extensions/nexus-signal/src/services/services.socket.ts @@ -0,0 +1,95 @@ +import { Container, Continuation, RequestContext, TasksService } from '@bitlerjs/nexus'; +import { tasks as notificationTasks } from '@bitlerjs/nexus-notifications'; + +import { Message } from '../types/message.js'; + +type SignalSocketOptions = { + id: string; + host: string; + secure: boolean; + container: Container; +}; + +class SignalSocket { + #options: SignalSocketOptions; + #socket: WebSocket; + + constructor(options: SignalSocketOptions) { + this.#options = options; + this.#socket = this.#setup(); + } + + public get id() { + return this.#options.id; + } + + #onopen = () => { + console.log('connected'); + }; + + #onclose = () => { + console.log('disconnected'); + }; + + #onerror = (event: Event) => { + console.error(event); + }; + + #onmessage = async (event: MessageEvent) => { + const { container } = this.#options; + const tasksService = container.get(TasksService); + const message: Message = JSON.parse(event.data); + if (message.envelope.dataMessage) { + await tasksService.run({ + task: notificationTasks.addNotification, + input: { + id: `signal-${message.envelope.sourceUuid}-${message.envelope.timestamp}`, + title: 'New signal message', + message: [ + `**From ${message.envelope.sourceName} (${message.envelope.source})**`, + '', + message.envelope.dataMessage.message, + ].join('\n'), + }, + requestContext: new RequestContext(), + continuation: new Continuation({ + container, + }), + }); + } + + if (message.envelope.syncMessage?.readMessages) { + const ids = message.envelope.syncMessage.readMessages.map( + (readMessage) => `signal-${readMessage.senderUuid}-${readMessage.timestamp}`, + ); + + await tasksService.run({ + task: notificationTasks.removeNotifications, + input: { + ids, + }, + requestContext: new RequestContext(), + continuation: new Continuation({ + container, + }), + }); + } + }; + + #setup = () => { + const { id, host, secure } = this.#options; + const socketUrl = new URL(`v1/receive/${id}`, `${secure ? 'wss' : 'ws'}://${host}`); + const socket = new WebSocket(socketUrl); + socket.addEventListener('message', this.#onmessage); + socket.addEventListener('open', this.#onopen); + socket.addEventListener('close', this.#onclose); + socket.addEventListener('error', this.#onerror); + return socket; + }; + + public destroy = () => { + this.#socket.close(); + }; +} + +export { SignalSocket, type SignalSocketOptions }; diff --git a/extensions/nexus-signal/src/tasks/tasks.get-contacts.ts b/extensions/nexus-signal/src/tasks/tasks.get-contacts.ts new file mode 100644 index 0000000..1e627d7 --- /dev/null +++ b/extensions/nexus-signal/src/tasks/tasks.get-contacts.ts @@ -0,0 +1,40 @@ +import { createTask, Type } from '@bitlerjs/nexus'; + +import { SignalService } from '../services/services.signal.js'; + +const getContactsTask = createTask({ + kind: 'signal.get-contacts', + name: 'Get Contacts', + group: 'Signal', + description: 'Get contacts from Signal', + input: Type.Object({}), + output: Type.Object({ + contacts: Type.Array( + Type.Object({ + name: Type.String(), + phone: Type.String(), + }), + ), + }), + handler: async ({ container }) => { + const signalService = container.get(SignalService); + const accounts = await signalService.get('/v1/accounts'); + const contactGroups = await Promise.all( + accounts.map(async (account) => { + const response = await signalService.get('/v1/contacts/{number}', { + path: { + number: account, + }, + }); + return response.map((contact) => ({ + name: contact.profile_name || contact.name || contact.username || contact.number || 'Unknown', + phone: contact.number || 'Unknown', + })); + }), + ); + const contacts = contactGroups.flatMap((a) => a); + return { contacts }; + }, +}); + +export { getContactsTask }; diff --git a/extensions/nexus-signal/src/tasks/tasks.get-groups.ts b/extensions/nexus-signal/src/tasks/tasks.get-groups.ts new file mode 100644 index 0000000..262c5d9 --- /dev/null +++ b/extensions/nexus-signal/src/tasks/tasks.get-groups.ts @@ -0,0 +1,42 @@ +import { createTask, Type } from '@bitlerjs/nexus'; + +import { SignalService } from '../services/services.signal.js'; + +const getGroupsTask = createTask({ + kind: 'signal.get-groups', + name: 'Get Groups', + group: 'Signal', + description: 'Get groups from Signal', + input: Type.Object({}), + output: Type.Object({ + groups: Type.Array( + Type.Object({ + name: Type.String(), + id: Type.String(), + internalId: Type.String(), + }), + ), + }), + handler: async ({ container }) => { + const signalService = container.get(SignalService); + const accounts = await signalService.get('/v1/accounts'); + const byAccount = await Promise.all( + accounts.map(async (account) => { + const response = await signalService.get('/v1/groups/{number}', { + path: { + number: account, + }, + }); + return response.map((contact) => ({ + id: contact.id || 'Unknown', + internalId: contact.internal_id || 'Unknown', + name: contact.name || 'Unknown', + })); + }), + ); + const groups = byAccount.flatMap((a) => a); + return { groups }; + }, +}); + +export { getGroupsTask }; diff --git a/extensions/nexus-signal/src/tasks/tasks.send.ts b/extensions/nexus-signal/src/tasks/tasks.send.ts new file mode 100644 index 0000000..04952c7 --- /dev/null +++ b/extensions/nexus-signal/src/tasks/tasks.send.ts @@ -0,0 +1,42 @@ +import { createTask, Type } from '@bitlerjs/nexus'; + +import { SignalService } from '../services/services.signal.js'; + +const sendTask = createTask({ + kind: 'signal.send', + name: 'Send Signal Message', + group: 'Signal', + description: 'Send a message to a Signal contact', + input: Type.Object({ + recipient: Type.String({ + description: "The recipient's phone number", + }), + message: Type.String(), + attachments: Type.Optional( + Type.Array( + Type.String({ + description: + 'Base64 encoded attachment ("", "data:;base64", "data:;filename=;base64")', + }), + ), + ), + }), + output: Type.Object({ + success: Type.Boolean(), + }), + handler: async ({ container, input }) => { + const signalService = container.get(SignalService); + const accounts = await signalService.getAccounts(); + await signalService.post('/v2/send', { + body: { + base64_attachments: input.attachments, + number: accounts[0], + recipients: [input.recipient], + message: input.message, + }, + }); + return { success: true }; + }, +}); + +export { sendTask }; diff --git a/extensions/nexus-signal/src/types/message.ts b/extensions/nexus-signal/src/types/message.ts new file mode 100644 index 0000000..74ca5ce --- /dev/null +++ b/extensions/nexus-signal/src/types/message.ts @@ -0,0 +1,67 @@ +type TypingMessage = { + action: 'STARTED' | 'STOPPED'; + timestamp: number; + groupId?: string; // Optional, only present in some cases +}; + +// Define the data message structure +type DataMessage = { + timestamp: number; + message: string; + expiresInSeconds: number; + viewOnce: boolean; +}; + +// Define the receipt message structure +type ReceiptMessage = { + when: number; + isDelivery: boolean; + isRead: boolean; + isViewed: boolean; + timestamps: number[]; +}; + +// Define the sync message structure +type SyncMessage = { + readMessages?: { + sender: string; + senderNumber: string; + senderUuid: string; + timestamp: number; + }[]; + sentMessage?: { + destination: string | null; + destinationNumber: string | null; + destinationUuid: string | null; + timestamp: number; + message: string; + expiresInSeconds: number; + viewOnce: boolean; + groupInfo?: { + groupId: string; + type: 'DELIVER'; + }; + }; +}; + +// Define the envelope structure +type Envelope = { + source: string; + sourceNumber: string; + sourceUuid: string; + sourceName: string; + sourceDevice: number; + timestamp: number; + typingMessage?: TypingMessage; + dataMessage?: DataMessage; + receiptMessage?: ReceiptMessage; + syncMessage?: SyncMessage; +}; + +// Define the message structure +type Message = { + envelope: Envelope; + account: string; +}; + +export { type TypingMessage, type DataMessage, type ReceiptMessage, type SyncMessage, type Envelope, type Message }; diff --git a/extensions/nexus-signal/tsconfig.json b/extensions/nexus-signal/tsconfig.json new file mode 100644 index 0000000..fadd53c --- /dev/null +++ b/extensions/nexus-signal/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@bitlerjs/nexus-config", + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/nexus/src/tasks/tasks.ts b/packages/nexus/src/tasks/tasks.ts index 6dc7d80..bda7b53 100644 --- a/packages/nexus/src/tasks/tasks.ts +++ b/packages/nexus/src/tasks/tasks.ts @@ -1,3 +1,5 @@ +import { Static, TSchema } from '@sinclair/typebox'; + import { Continuation } from '../continuation/continuation.js'; import { Container } from '../container/container.js'; import { RequestContext } from '../request-context/request-context.js'; @@ -8,9 +10,9 @@ import { EventsService } from '../events/events.js'; import { Task } from './tasks.task.js'; import { tasksUpdated } from './tasks.events.js'; -type RunOptions = { - task: Task; - input: unknown; +type RunOptions = { + task: Task; + input: Static; requestContext: RequestContext; continuation: Continuation; }; @@ -39,7 +41,7 @@ class TasksService { return this.#tasks.get(kind); }; - public run = async (options: RunOptions) => { + public run = async (options: RunOptions) => { const { task, requestContext, continuation, input } = options; const sources = new Sources({ container: this.#container, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 674e479..d35d80c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,12 +77,18 @@ importers: '@bitlerjs/nexus-notes': specifier: workspace:* version: link:../../extensions/nexus-notes + '@bitlerjs/nexus-notifications': + specifier: workspace:* + version: link:../../extensions/nexus-notifications '@bitlerjs/nexus-openai': specifier: workspace:* version: link:../../packages/nexus-openai '@bitlerjs/nexus-openapi': specifier: workspace:* version: link:../../packages/nexus-openapi + '@bitlerjs/nexus-signal': + specifier: workspace:* + version: link:../../extensions/nexus-signal '@bitlerjs/nexus-studio': specifier: workspace:* version: link:../../packages/nexus-studio @@ -273,6 +279,56 @@ importers: specifier: ^5.7.2 version: 5.7.3 + extensions/nexus-notifications: + dependencies: + '@bitlerjs/nexus': + specifier: workspace:* + version: link:../../packages/nexus + '@bitlerjs/nexus-data': + specifier: workspace:* + version: link:../../packages/nexus-data + nanoid: + specifier: ^5.0.9 + version: 5.0.9 + devDependencies: + '@bitlerjs/nexus-config': + specifier: workspace:* + version: link:../../packages/nexus-config + typescript: + specifier: ^5.7.2 + version: 5.7.3 + + extensions/nexus-signal: + dependencies: + '@bitlerjs/nexus': + specifier: workspace:* + version: link:../../packages/nexus + '@bitlerjs/nexus-configs': + specifier: workspace:* + version: link:../../packages/nexus-configs + '@bitlerjs/nexus-data': + specifier: workspace:* + version: link:../../packages/nexus-data + '@bitlerjs/nexus-notifications': + specifier: workspace:* + version: link:../nexus-notifications + nanoid: + specifier: ^5.0.9 + version: 5.0.9 + openapi-fetch: + specifier: ^0.13.3 + version: 0.13.4 + devDependencies: + '@bitlerjs/nexus-config': + specifier: workspace:* + version: link:../../packages/nexus-config + openapi-typescript: + specifier: ^5.4.2 + version: 5.4.2 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + extensions/nexus-timers: dependencies: '@bitlerjs/nexus': @@ -1136,6 +1192,10 @@ packages: '@fastify/ajv-compiler@4.0.2': resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@fastify/cors@10.0.2': resolution: {integrity: sha512-DGdxOG36sS/tZv1NFiCJGi7wGuXOSPL2CmNX5PbOVKx0C6LuIALRMrqLByHTCcX1Rbl8NJ9IWlJex32bzydvlw==} @@ -4093,6 +4153,12 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -5019,9 +5085,20 @@ packages: zod: optional: true + openapi-fetch@0.13.4: + resolution: {integrity: sha512-JHX7UYjLEiHuQGCPxa3CCCIqe/nc4bTIF9c4UYVC8BegAbWoS3g4gJxKX5XcG7UtYQs2060kY6DH64KkvNZahg==} + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + openapi-typescript-helpers@0.0.15: + resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + + openapi-typescript@5.4.2: + resolution: {integrity: sha512-tHeRv39Yh7brqJpbUntdjtUaXrTHmC4saoyTLU/0J2I8LEFQYDXRLgnmWTMiMOB2GXugJiqHa5n9sAyd6BRqiA==} + engines: {node: '>= 14.0.0'} + hasBin: true + openid-client@6.1.7: resolution: {integrity: sha512-JfY/KvQgOutmG2P+oVNKInE7zIh+im1MQOaO7g5CtNnTWMociA563WweiEMKfR9ry9XG3K2HGvj9wEqhCQkPMg==} @@ -5213,6 +5290,11 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + prettier@3.4.2: resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} engines: {node: '>=14'} @@ -5682,6 +5764,9 @@ packages: resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} engines: {node: '>=8'} + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -5838,6 +5923,10 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici@5.28.5: + resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} + engines: {node: '>=14.0'} + unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} @@ -6144,6 +6233,10 @@ packages: engines: {node: '>= 14'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -6394,6 +6487,8 @@ snapshots: ajv-formats: 3.0.1(ajv@8.17.1) fast-uri: 3.0.6 + '@fastify/busboy@2.1.1': {} + '@fastify/cors@10.0.2': dependencies: fastify-plugin: 5.0.1 @@ -10495,6 +10590,10 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + globalyzer@0.1.0: {} + + globrex@0.1.2: {} + gopd@1.2.0: {} graceful-fs@4.2.10: {} @@ -11543,8 +11642,23 @@ snapshots: transitivePeerDependencies: - encoding + openapi-fetch@0.13.4: + dependencies: + openapi-typescript-helpers: 0.0.15 + openapi-types@12.1.3: {} + openapi-typescript-helpers@0.0.15: {} + + openapi-typescript@5.4.2: + dependencies: + js-yaml: 4.1.0 + mime: 3.0.0 + prettier: 2.8.8 + tiny-glob: 0.2.9 + undici: 5.28.5 + yargs-parser: 21.1.1 + openid-client@6.1.7: dependencies: jose: 5.9.6 @@ -11745,6 +11859,8 @@ snapshots: dependencies: fast-diff: 1.3.0 + prettier@2.8.8: {} + prettier@3.4.2: {} pretty-bytes@5.6.0: {} @@ -12332,6 +12448,11 @@ snapshots: tildify@2.0.0: {} + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -12486,6 +12607,10 @@ snapshots: undici-types@6.20.0: {} + undici@5.28.5: + dependencies: + '@fastify/busboy': 2.1.1 + unfetch@4.2.0: {} unified@11.0.5: @@ -12777,6 +12902,8 @@ snapshots: yaml@2.7.0: {} + yargs-parser@21.1.1: {} + yocto-queue@0.1.0: {} zhead@2.2.4: {} From 92ab519a6d98a2d4e6bad1e5b7f875be950170f5 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Tue, 18 Feb 2025 21:22:14 +0100 Subject: [PATCH 2/2] fix configurability --- extensions/nexus-signal/package.json | 2 +- .../nexus-signal/src/configs/configs.ts | 3 +- extensions/nexus-signal/src/generated/api.ts | 340 +++++++++--------- .../src/services/services.signal.ts | 72 ++-- pnpm-lock.yaml | 10 +- 5 files changed, 223 insertions(+), 204 deletions(-) diff --git a/extensions/nexus-signal/package.json b/extensions/nexus-signal/package.json index 67d66e3..d1d135a 100644 --- a/extensions/nexus-signal/package.json +++ b/extensions/nexus-signal/package.json @@ -22,6 +22,6 @@ "@bitlerjs/nexus-data": "workspace:*", "@bitlerjs/nexus-notifications": "workspace:*", "nanoid": "^5.0.9", - "openapi-fetch": "^0.13.3" + "openapi-fetch": "^0.12.5" } } diff --git a/extensions/nexus-signal/src/configs/configs.ts b/extensions/nexus-signal/src/configs/configs.ts index bf7a444..8d56312 100644 --- a/extensions/nexus-signal/src/configs/configs.ts +++ b/extensions/nexus-signal/src/configs/configs.ts @@ -7,7 +7,8 @@ const signalConfig = createConfig({ group: 'Integrations', description: 'Signal integration', schema: Type.Object({ - apiUrl: Type.String(), + host: Type.String(), + secure: Type.Boolean(), }), }); diff --git a/extensions/nexus-signal/src/generated/api.ts b/extensions/nexus-signal/src/generated/api.ts index 975ba7f..043af64 100644 --- a/extensions/nexus-signal/src/generated/api.ts +++ b/extensions/nexus-signal/src/generated/api.ts @@ -3,19 +3,19 @@ * Do not make direct changes to the file. */ -export type paths = { - '/v1/about': { +export interface paths { + "/v1/about": { /** Returns the supported API versions and the internal build nr */ get: { responses: { /** OK */ 200: { - schema: definitions['client.About']; + schema: definitions["client.About"]; }; }; }; }; - '/v1/accounts': { + "/v1/accounts": { /** Lists all of the accounts linked or registered */ get: { responses: { @@ -25,12 +25,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/accounts/{number}/rate-limit-challenge': { + "/v1/accounts/{number}/rate-limit-challenge": { /** When running into rate limits, sometimes the limit can be lifted, by solving a CAPTCHA. To get the captcha token, go to https://signalcaptchas.org/challenge/generate.html For the staging environment, use: https://signalcaptchas.org/staging/registration/generate.html. The "challenge_token" is the token from the failed send attempt. The "captcha" is the captcha result, starting with signalcaptcha:// */ post: { parameters: { @@ -40,7 +40,7 @@ export type paths = { }; body: { /** Request */ - data: definitions['api.RateLimitChallengeRequest']; + data: definitions["api.RateLimitChallengeRequest"]; }; }; responses: { @@ -48,12 +48,12 @@ export type paths = { 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/accounts/{number}/settings': { + "/v1/accounts/{number}/settings": { /** Update the account attributes on the signal server. */ put: { parameters: { @@ -63,7 +63,7 @@ export type paths = { }; body: { /** Request */ - data: definitions['api.UpdateAccountSettingsRequest']; + data: definitions["api.UpdateAccountSettingsRequest"]; }; }; responses: { @@ -71,12 +71,12 @@ export type paths = { 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/accounts/{number}/username': { + "/v1/accounts/{number}/username": { /** Allows to set the username that should be used for this account. This can either be just the nickname (e.g. test) or the complete username with discriminator (e.g. test.123). Returns the new username with discriminator and the username link. */ post: { parameters: { @@ -86,19 +86,19 @@ export type paths = { }; body: { /** Request */ - data: definitions['api.SetUsernameRequest']; + data: definitions["api.SetUsernameRequest"]; }; }; responses: { /** Created */ 201: { - schema: definitions['client.SetUsernameResponse']; + schema: definitions["client.SetUsernameResponse"]; }; /** No Content */ 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -115,12 +115,12 @@ export type paths = { 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/attachments': { + "/v1/attachments": { /** List all downloaded attachments */ get: { responses: { @@ -130,12 +130,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/attachments/{attachment}': { + "/v1/attachments/{attachment}": { /** Serve the attachment with the given id */ get: { parameters: { @@ -151,7 +151,7 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -170,22 +170,22 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/configuration': { + "/v1/configuration": { /** List the REST API configuration. */ get: { responses: { /** OK */ 200: { - schema: definitions['api.Configuration']; + schema: definitions["api.Configuration"]; }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -194,7 +194,7 @@ export type paths = { parameters: { body: { /** Configuration */ - data: definitions['api.Configuration']; + data: definitions["api.Configuration"]; }; }; responses: { @@ -204,12 +204,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/configuration/{number}/settings': { + "/v1/configuration/{number}/settings": { /** List account specific settings. */ get: { parameters: { @@ -219,7 +219,7 @@ export type paths = { }; body: { /** Request */ - data: definitions['api.TrustModeResponse']; + data: definitions["api.TrustModeResponse"]; }; }; responses: { @@ -227,7 +227,7 @@ export type paths = { 200: unknown; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -240,7 +240,7 @@ export type paths = { }; body: { /** Request */ - data: definitions['api.TrustModeRequest']; + data: definitions["api.TrustModeRequest"]; }; }; responses: { @@ -248,12 +248,12 @@ export type paths = { 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/contacts/{number}': { + "/v1/contacts/{number}": { /** List all contacts for the given number. */ get: { parameters: { @@ -265,7 +265,7 @@ export type paths = { responses: { /** OK */ 200: { - schema: definitions['client.ListContactsResponse'][]; + schema: definitions["client.ListContactsResponse"][]; }; }; }; @@ -278,7 +278,7 @@ export type paths = { }; body: { /** Contact */ - data: definitions['api.UpdateContactRequest']; + data: definitions["api.UpdateContactRequest"]; }; }; responses: { @@ -286,12 +286,12 @@ export type paths = { 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/contacts/{number}/sync': { + "/v1/contacts/{number}/sync": { /** Send a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the primary device. */ post: { parameters: { @@ -305,12 +305,12 @@ export type paths = { 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/devices/{number}': { + "/v1/devices/{number}": { /** Links another device to this device. Only works, if this is the master device. */ post: { parameters: { @@ -320,7 +320,7 @@ export type paths = { }; body: { /** Request */ - data: definitions['api.AddDeviceRequest']; + data: definitions["api.AddDeviceRequest"]; }; }; responses: { @@ -328,12 +328,12 @@ export type paths = { 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/groups/{number}': { + "/v1/groups/{number}": { /** List all Signal Groups. */ get: { parameters: { @@ -345,11 +345,11 @@ export type paths = { responses: { /** OK */ 200: { - schema: definitions['client.GroupEntry'][]; + schema: definitions["client.GroupEntry"][]; }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -358,7 +358,7 @@ export type paths = { parameters: { body: { /** Input Data */ - data: definitions['api.CreateGroupRequest']; + data: definitions["api.CreateGroupRequest"]; }; path: { /** Registered Phone Number */ @@ -368,16 +368,16 @@ export type paths = { responses: { /** Created */ 201: { - schema: definitions['api.CreateGroupResponse']; + schema: definitions["api.CreateGroupResponse"]; }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/groups/{number}/{groupid}': { + "/v1/groups/{number}/{groupid}": { /** List a specific Signal Group. */ get: { parameters: { @@ -391,11 +391,11 @@ export type paths = { responses: { /** OK */ 200: { - schema: definitions['client.GroupEntry']; + schema: definitions["client.GroupEntry"]; }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -410,7 +410,7 @@ export type paths = { }; body: { /** Input Data */ - data: definitions['api.UpdateGroupRequest']; + data: definitions["api.UpdateGroupRequest"]; }; }; responses: { @@ -420,7 +420,7 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -441,18 +441,18 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/groups/{number}/{groupid}/admins': { + "/v1/groups/{number}/{groupid}/admins": { /** Add one or more admins to an existing Signal Group. */ post: { parameters: { body: { /** Admins */ - data: definitions['api.ChangeGroupAdminsRequest']; + data: definitions["api.ChangeGroupAdminsRequest"]; }; path: { /** Registered Phone Number */ @@ -468,7 +468,7 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -477,7 +477,7 @@ export type paths = { parameters: { body: { /** Admins */ - data: definitions['api.ChangeGroupAdminsRequest']; + data: definitions["api.ChangeGroupAdminsRequest"]; }; path: { /** Registered Phone Number */ @@ -493,12 +493,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/groups/{number}/{groupid}/block': { + "/v1/groups/{number}/{groupid}/block": { /** Block the specified Signal Group. */ post: { parameters: { @@ -516,12 +516,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/groups/{number}/{groupid}/join': { + "/v1/groups/{number}/{groupid}/join": { /** Join the specified Signal Group. */ post: { parameters: { @@ -539,18 +539,18 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/groups/{number}/{groupid}/members': { + "/v1/groups/{number}/{groupid}/members": { /** Add one or more members to an existing Signal Group. */ post: { parameters: { body: { /** Members */ - data: definitions['api.ChangeGroupMembersRequest']; + data: definitions["api.ChangeGroupMembersRequest"]; }; path: { /** Registered Phone Number */ @@ -566,7 +566,7 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -575,7 +575,7 @@ export type paths = { parameters: { body: { /** Members */ - data: definitions['api.ChangeGroupMembersRequest']; + data: definitions["api.ChangeGroupMembersRequest"]; }; path: { /** Registered Phone Number */ @@ -591,12 +591,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/groups/{number}/{groupid}/quit': { + "/v1/groups/{number}/{groupid}/quit": { /** Quit the specified Signal Group. */ post: { parameters: { @@ -614,12 +614,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/health': { + "/v1/health": { /** Internally used by the docker container to perform the health check. */ get: { responses: { @@ -630,7 +630,7 @@ export type paths = { }; }; }; - '/v1/identities/{number}': { + "/v1/identities/{number}": { /** List all identities for the given number. */ get: { parameters: { @@ -642,18 +642,18 @@ export type paths = { responses: { /** OK */ 200: { - schema: definitions['client.IdentityEntry'][]; + schema: definitions["client.IdentityEntry"][]; }; }; }; }; - '/v1/identities/{number}/trust/{numberToTrust}': { + "/v1/identities/{number}/trust/{numberToTrust}": { /** Trust an identity. When 'trust_all_known_keys' is set to' true', all known keys of this user are trusted. **This is only recommended for testing.** */ put: { parameters: { body: { /** Input Data */ - data: definitions['api.TrustIdentityRequest']; + data: definitions["api.TrustIdentityRequest"]; }; path: { /** Registered Phone Number */ @@ -670,13 +670,13 @@ export type paths = { }; }; }; - '/v1/profiles/{number}': { + "/v1/profiles/{number}": { /** Set your name and optional an avatar. */ put: { parameters: { body: { /** Profile Data */ - data: definitions['api.UpdateProfileRequest']; + data: definitions["api.UpdateProfileRequest"]; }; path: { /** Registered Phone Number */ @@ -690,12 +690,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/qrcodelink': { + "/v1/qrcodelink": { /** Link device and generate QR code */ get: { parameters: { @@ -713,18 +713,18 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/reactions/{number}': { + "/v1/reactions/{number}": { /** React to a message */ post: { parameters: { body: { /** Reaction */ - data: definitions['api.Reaction']; + data: definitions["api.Reaction"]; }; path: { /** Registered phone number */ @@ -738,7 +738,7 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -747,7 +747,7 @@ export type paths = { parameters: { body: { /** Reaction */ - data: definitions['api.Reaction']; + data: definitions["api.Reaction"]; }; path: { /** Registered phone number */ @@ -761,18 +761,18 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/receipts/{number}': { + "/v1/receipts/{number}": { /** Send a read or viewed receipt */ post: { parameters: { body: { /** Receipt */ - data: definitions['api.Receipt']; + data: definitions["api.Receipt"]; }; path: { /** Registered phone number */ @@ -786,12 +786,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/receive/{number}': { + "/v1/receive/{number}": { /** Receives Signal Messages from the Signal Network. If you are running the docker container in normal/native mode, this is a GET endpoint. In json-rpc mode this is a websocket endpoint. */ get: { parameters: { @@ -819,12 +819,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/register/{number}': { + "/v1/register/{number}": { /** Register a phone number with the signal network. */ post: { parameters: { @@ -834,7 +834,7 @@ export type paths = { }; body: { /** Additional Settings */ - data?: definitions['api.RegisterNumberRequest']; + data?: definitions["api.RegisterNumberRequest"]; }; }; responses: { @@ -842,12 +842,12 @@ export type paths = { 201: unknown; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/register/{number}/verify/{token}': { + "/v1/register/{number}/verify/{token}": { /** Verify a registered phone number with the signal network. */ post: { parameters: { @@ -859,7 +859,7 @@ export type paths = { }; body: { /** Additional Settings */ - data?: definitions['api.VerifyNumberSettings']; + data?: definitions["api.VerifyNumberSettings"]; }; }; responses: { @@ -869,12 +869,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/search/{number}': { + "/v1/search/{number}": { /** Check if one or more phone numbers are registered with the Signal Service. */ get: { parameters: { @@ -890,22 +890,22 @@ export type paths = { responses: { /** OK */ 200: { - schema: definitions['api.SearchResponse'][]; + schema: definitions["api.SearchResponse"][]; }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/send': { + "/v1/send": { /** Send a signal message */ post: { parameters: { body: { /** Input Data */ - data: definitions['api.SendMessageV1']; + data: definitions["api.SendMessageV1"]; }; }; responses: { @@ -915,12 +915,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/sticker-packs/{number}': { + "/v1/sticker-packs/{number}": { /** List Installed Sticker Packs. */ get: { parameters: { @@ -932,13 +932,13 @@ export type paths = { responses: { /** OK */ 200: { - schema: definitions['client.ListInstalledStickerPacksResponse'][]; + schema: definitions["client.ListInstalledStickerPacksResponse"][]; }; /** No Content */ 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -951,7 +951,7 @@ export type paths = { }; body: { /** Request */ - data: definitions['api.AddStickerPackRequest']; + data: definitions["api.AddStickerPackRequest"]; }; }; responses: { @@ -959,12 +959,12 @@ export type paths = { 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/typing-indicator/{number}': { + "/v1/typing-indicator/{number}": { /** Show Typing Indicator. */ put: { parameters: { @@ -974,7 +974,7 @@ export type paths = { }; body: { /** Type */ - data: definitions['api.TypingIndicatorRequest']; + data: definitions["api.TypingIndicatorRequest"]; }; }; responses: { @@ -984,7 +984,7 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; @@ -997,7 +997,7 @@ export type paths = { }; body: { /** Type */ - data: definitions['api.TypingIndicatorRequest']; + data: definitions["api.TypingIndicatorRequest"]; }; }; responses: { @@ -1007,12 +1007,12 @@ export type paths = { }; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v1/unregister/{number}': { + "/v1/unregister/{number}": { /** Disables push support for this device. **WARNING:** If *delete_account* is set to *true*, the account will be deleted from the Signal Server. This cannot be undone without loss. */ post: { parameters: { @@ -1022,7 +1022,7 @@ export type paths = { }; body: { /** Additional Settings */ - data?: definitions['api.UnregisterNumberRequest']; + data?: definitions["api.UnregisterNumberRequest"]; }; }; responses: { @@ -1030,112 +1030,112 @@ export type paths = { 204: never; /** Bad Request */ 400: { - schema: definitions['api.Error']; + schema: definitions["api.Error"]; }; }; }; }; - '/v2/send': { - /** Send a signal message. Set the text_mode to 'styled' in case you want to add formatting to your text message. Styling Options: *italic text*, **bold text**, ~strikethrough text~. If you want to escape a formatting character, prefix it with two backslashes ('\\') */ + "/v2/send": { + /** Send a signal message. Set the text_mode to 'styled' in case you want to add formatting to your text message. Styling Options: \*italic text\*, \*\*bold text\*\*, ~strikethrough text~, ||spoiler||, \`monospace\`. If you want to escape a formatting character, prefix it with two backslashes. */ post: { parameters: { body: { /** Input Data */ - data: definitions['api.SendMessageV2']; + data: definitions["api.SendMessageV2"]; }; }; responses: { /** Created */ 201: { - schema: definitions['api.SendMessageResponse']; + schema: definitions["api.SendMessageResponse"]; }; /** Bad Request */ 400: { - schema: definitions['api.SendMessageError']; + schema: definitions["api.SendMessageError"]; }; }; }; }; -}; +} -export type definitions = { - 'api.AddDeviceRequest': { +export interface definitions { + "api.AddDeviceRequest": { uri?: string; }; - 'api.AddStickerPackRequest': { + "api.AddStickerPackRequest": { /** @example 9a32eda01a7a28574f2eb48668ae0dc4 */ pack_id?: string; /** @example 19546e18eba0ff69dea78eb591465289d39e16f35e58389ae779d4f9455aff3a */ pack_key?: string; }; - 'api.ChangeGroupAdminsRequest': { + "api.ChangeGroupAdminsRequest": { admins?: string[]; }; - 'api.ChangeGroupMembersRequest': { + "api.ChangeGroupMembersRequest": { members?: string[]; }; - 'api.Configuration': { - logging?: definitions['api.LoggingConfiguration']; + "api.Configuration": { + logging?: definitions["api.LoggingConfiguration"]; }; - 'api.CreateGroupRequest': { + "api.CreateGroupRequest": { description?: string; expiration_time?: number; /** @enum {string} */ - group_link?: 'disabled' | 'enabled' | 'enabled-with-approval'; + group_link?: "disabled" | "enabled" | "enabled-with-approval"; members?: string[]; name?: string; - permissions?: definitions['api.GroupPermissions']; + permissions?: definitions["api.GroupPermissions"]; }; - 'api.CreateGroupResponse': { + "api.CreateGroupResponse": { id?: string; }; - 'api.Error': { + "api.Error": { error?: string; }; - 'api.GroupPermissions': { + "api.GroupPermissions": { /** @enum {string} */ - add_members?: 'only-admins' | 'every-member'; + add_members?: "only-admins" | "every-member"; /** @enum {string} */ - edit_group?: 'only-admins' | 'every-member'; + edit_group?: "only-admins" | "every-member"; }; - 'api.LoggingConfiguration': { + "api.LoggingConfiguration": { Level?: string; }; - 'api.RateLimitChallengeRequest': { + "api.RateLimitChallengeRequest": { /** @example signalcaptcha://{captcha value} */ captcha?: string; /** @example */ challenge_token?: string; }; - 'api.Reaction': { + "api.Reaction": { reaction?: string; recipient?: string; target_author?: string; timestamp?: number; }; - 'api.Receipt': { + "api.Receipt": { /** @enum {string} */ - receipt_type?: 'read' | 'viewed'; + receipt_type?: "read" | "viewed"; recipient?: string; timestamp?: number; }; - 'api.RegisterNumberRequest': { + "api.RegisterNumberRequest": { captcha?: string; use_voice?: boolean; }; - 'api.SearchResponse': { + "api.SearchResponse": { number?: string; registered?: boolean; }; - 'api.SendMessageError': { + "api.SendMessageError": { account?: string; challenge_tokens?: string[]; error?: string; }; - 'api.SendMessageResponse': { + "api.SendMessageResponse": { timestamp?: string; }; - 'api.SendMessageV1': { + "api.SendMessageV1": { /** @example '' OR 'data:;base64,' OR 'data:;filename=;base64,' */ base64_attachment?: string; is_group?: boolean; @@ -1143,7 +1143,7 @@ export type definitions = { number?: string; recipients?: string[]; }; - 'api.SendMessageV2': { + "api.SendMessageV2": { /** * @example [ * "", @@ -1153,74 +1153,74 @@ export type definitions = { */ base64_attachments?: string[]; edit_timestamp?: number; - mentions?: definitions['data.MessageMention'][]; + mentions?: definitions["data.MessageMention"][]; message?: string; notify_self?: boolean; number?: string; quote_author?: string; - quote_mentions?: definitions['data.MessageMention'][]; + quote_mentions?: definitions["data.MessageMention"][]; quote_message?: string; quote_timestamp?: number; recipients?: string[]; sticker?: string; /** @enum {string} */ - text_mode?: 'normal' | 'styled'; + text_mode?: "normal" | "styled"; }; - 'api.SetUsernameRequest': { + "api.SetUsernameRequest": { /** @example test */ username?: string; }; - 'api.TrustIdentityRequest': { + "api.TrustIdentityRequest": { /** @example false */ trust_all_known_keys?: boolean; verified_safety_number?: string; }; - 'api.TrustModeRequest': { + "api.TrustModeRequest": { trust_mode?: string; }; - 'api.TrustModeResponse': { + "api.TrustModeResponse": { trust_mode?: string; }; - 'api.TypingIndicatorRequest': { + "api.TypingIndicatorRequest": { recipient?: string; }; - 'api.UnregisterNumberRequest': { + "api.UnregisterNumberRequest": { /** @example false */ delete_account?: boolean; /** @example false */ delete_local_data?: boolean; }; - 'api.UpdateAccountSettingsRequest': { + "api.UpdateAccountSettingsRequest": { discoverable_by_number?: boolean; share_number?: boolean; }; - 'api.UpdateContactRequest': { + "api.UpdateContactRequest": { expiration_in_seconds?: number; name?: string; recipient?: string; }; - 'api.UpdateGroupRequest': { + "api.UpdateGroupRequest": { base64_avatar?: string; description?: string; expiration_time?: number; name?: string; }; - 'api.UpdateProfileRequest': { + "api.UpdateProfileRequest": { about?: string; base64_avatar?: string; name?: string; }; - 'api.VerifyNumberSettings': { + "api.VerifyNumberSettings": { pin?: string; }; - 'client.About': { + "client.About": { build?: number; - capabilities?: Record; + capabilities?: { [key: string]: string[] }; mode?: string; version?: string; versions?: string[]; }; - 'client.GroupEntry': { + "client.GroupEntry": { admins?: string[]; blocked?: boolean; id?: string; @@ -1231,14 +1231,14 @@ export type definitions = { pending_invites?: string[]; pending_requests?: string[]; }; - 'client.IdentityEntry': { + "client.IdentityEntry": { added?: string; fingerprint?: string; number?: string; safety_number?: string; status?: string; }; - 'client.ListContactsResponse': { + "client.ListContactsResponse": { blocked?: boolean; color?: string; message_expiration?: string; @@ -1248,24 +1248,24 @@ export type definitions = { username?: string; uuid?: string; }; - 'client.ListInstalledStickerPacksResponse': { + "client.ListInstalledStickerPacksResponse": { author?: string; installed?: boolean; pack_id?: string; title?: string; url?: string; }; - 'client.SetUsernameResponse': { + "client.SetUsernameResponse": { username?: string; username_link?: string; }; - 'data.MessageMention': { + "data.MessageMention": { author?: string; length?: number; start?: number; }; -}; +} -export type operations = {}; +export interface operations {} -export type external = {}; +export interface external {} diff --git a/extensions/nexus-signal/src/services/services.signal.ts b/extensions/nexus-signal/src/services/services.signal.ts index 3334a7e..f47f6a4 100644 --- a/extensions/nexus-signal/src/services/services.signal.ts +++ b/extensions/nexus-signal/src/services/services.signal.ts @@ -7,48 +7,63 @@ import { SignalSocket } from './services.socket.js'; type ApiReponse = TMethod extends keyof paths[TPath] ? paths[TPath][TMethod] extends { responses: { 200: { schema: infer U } } } - ? U - : never + ? U + : never : never; type ApiBody = TMethod extends keyof paths[TPath] ? paths[TPath][TMethod] extends { parameters: { body: { data: infer U } } } - ? U - : never + ? U + : never : never; type ApiPathParamters = TMethod extends keyof paths[TPath] ? paths[TPath][TMethod] extends { parameters: { path: infer U } } - ? U - : never + ? U + : never : never; type ApiQueryParameters = TMethod extends keyof paths[TPath] ? paths[TPath][TMethod] extends { parameters: { query: infer U } } - ? U - : never + ? U + : never : never; -// TODO: FIX -const host = 'localhost'; -const secure = false; - class SignalService { - #client: Client; #container: Container; - #setupPromise: Promise | null = null; + #config?: { + host: string; + secure: boolean; + }; + #setupPromise?: Promise<{ + client: Client; + sockets: SignalSocket[]; + }>; constructor(container: Container) { this.#container = container; - this.#client = createClient({ - host, - secure, - }); + } + + public set config(config: { host: string; secure: boolean }) { + this.#config = config; + this.destroy(); } #setup = async () => { - const accounts = await this.get('/v1/accounts'); - if (!accounts) { + if (!this.#config) { + throw new Error('No config'); + } + const { host, secure } = this.#config; + const client = createClient({ + host, + secure, + }); + const response = await client.GET('/v1/accounts'); + if (response.error) { + throw new Error(response.error instanceof Error ? response.error.message : 'API error'); + } + const accounts = response.data as string[]; + if (!accounts || accounts.length === 0) { throw new Error('No accounts found'); } const sockets = accounts.map( @@ -61,7 +76,7 @@ class SignalService { }), ); - return sockets; + return { client, sockets }; }; public setup = async () => { @@ -75,14 +90,15 @@ class SignalService { if (!this.#setupPromise) { return; } - - const sockets = await this.#setupPromise; + const promise = this.#setupPromise; + this.#setupPromise = undefined; + const { sockets } = await promise; sockets.forEach((socket) => socket.destroy()); }; public getAccounts = async () => { - const accounts = await this.setup(); - return accounts.map((account) => account.id); + const { sockets } = await this.setup(); + return sockets.map((account) => account.id); }; public get = async ( @@ -92,7 +108,8 @@ class SignalService { path?: ApiPathParamters; } = {}, ): Promise> => { - const { data, error } = await this.#client.GET(path as any, { + const { client } = await this.setup(); + const { data, error } = await client.GET(path as any, { params: { query: params.query, path: params.path, @@ -113,7 +130,8 @@ class SignalService { path?: ApiPathParamters; }, ): Promise> => { - const { data, error } = await this.#client.POST(path as any, { + const { client } = await this.setup(); + const { data, error } = await client.POST(path as any, { body: params.body, params: { query: params.query, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d35d80c..7be3070 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -316,8 +316,8 @@ importers: specifier: ^5.0.9 version: 5.0.9 openapi-fetch: - specifier: ^0.13.3 - version: 0.13.4 + specifier: ^0.12.5 + version: 0.12.5 devDependencies: '@bitlerjs/nexus-config': specifier: workspace:* @@ -5085,8 +5085,8 @@ packages: zod: optional: true - openapi-fetch@0.13.4: - resolution: {integrity: sha512-JHX7UYjLEiHuQGCPxa3CCCIqe/nc4bTIF9c4UYVC8BegAbWoS3g4gJxKX5XcG7UtYQs2060kY6DH64KkvNZahg==} + openapi-fetch@0.12.5: + resolution: {integrity: sha512-FnAMWLt0MNL6ComcL4q/YbB1tUgyz5YnYtwA1+zlJ5xcucmK5RlWsgH1ynxmEeu8fGJkYjm8armU/HVpORc9lw==} openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -11642,7 +11642,7 @@ snapshots: transitivePeerDependencies: - encoding - openapi-fetch@0.13.4: + openapi-fetch@0.12.5: dependencies: openapi-typescript-helpers: 0.0.15