metamigo-api: work on making it build
This commit is contained in:
parent
38e68852d9
commit
ef216f7b1c
35 changed files with 407 additions and 322 deletions
|
|
@ -2,25 +2,11 @@ require("eslint-config-link/patch/modern-module-resolution");
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
"eslint-config-link/profile/node",
|
"eslint-config-link/profile/node",
|
||||||
"eslint-config-link/eslint-config-amigo/profile/typescript",
|
"eslint-config-link/profile/typescript",
|
||||||
"eslint-config-link/eslint-config-amigo/profile/jest",
|
"eslint-config-link/profile/jest",
|
||||||
],
|
],
|
||||||
parserOptions: { tsconfigRootDir: __dirname },
|
parserOptions: { tsconfigRootDir: __dirname },
|
||||||
rules: {
|
rules: {
|
||||||
"new-cap": "off",
|
"new-cap": "off"
|
||||||
"import/no-extraneous-dependencies": [
|
|
||||||
// enable this when this is fixed
|
|
||||||
// https://github.com/benmosher/eslint-plugin-import/pull/1696
|
|
||||||
"off",
|
|
||||||
{
|
|
||||||
packageDir: [
|
|
||||||
".",
|
|
||||||
"node_modules/@digiresilience/amigo",
|
|
||||||
"node_modules/@digiresilience/amigo-dev",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// TODO: enable this after jest fixes this issue https://github.com/nodejs/node/issues/38343
|
|
||||||
"unicorn/prefer-node-protocol": "off"
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import type * as Hapi from "@hapi/hapi";
|
|
||||||
import Schmervice from "@hapipal/schmervice";
|
|
||||||
import PgPromisePlugin from "@digiresilience/hapi-pg-promise";
|
|
||||||
|
|
||||||
import type { IAppConfig } from "../../config";
|
|
||||||
import { dbInitOptions } from "@digiresilience/metamigo-db";
|
|
||||||
import { registerNextAuth } from "./hapi-nextauth";
|
|
||||||
import { registerSwagger } from "./swagger";
|
|
||||||
import { registerNextAuthJwt } from "./nextauth-jwt";
|
|
||||||
import { registerCloudflareAccessJwt } from "./cloudflare-jwt";
|
|
||||||
|
|
||||||
export const register = async (
|
|
||||||
server: Hapi.Server,
|
|
||||||
config: IAppConfig
|
|
||||||
): Promise<void> => {
|
|
||||||
await server.register(Schmervice);
|
|
||||||
|
|
||||||
await server.register({
|
|
||||||
plugin: PgPromisePlugin,
|
|
||||||
options: {
|
|
||||||
// the only required parameter is the connection string
|
|
||||||
connection: config.db.connection,
|
|
||||||
// ... and the pg-promise initialization options
|
|
||||||
pgpInit: dbInitOptions(config),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await registerNextAuth(server, config);
|
|
||||||
await registerSwagger(server);
|
|
||||||
await registerNextAuthJwt(server, config);
|
|
||||||
await registerCloudflareAccessJwt(server, config);
|
|
||||||
};
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import config, {
|
|
||||||
loadConfig,
|
|
||||||
loadConfigRaw,
|
|
||||||
IAppConfig,
|
|
||||||
IAppConvict,
|
|
||||||
} from "config";
|
|
||||||
|
|
||||||
export { IAppConvict, IAppConfig, loadConfig, loadConfigRaw };
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|
@ -37,7 +37,8 @@
|
||||||
"pg-promise": "^11.0.2",
|
"pg-promise": "^11.0.2",
|
||||||
"postgraphile-plugin-connection-filter": "^2.3.0",
|
"postgraphile-plugin-connection-filter": "^2.3.0",
|
||||||
"remeda": "^1.6.0",
|
"remeda": "^1.6.0",
|
||||||
"twilio": "^3.84.1"
|
"twilio": "^3.84.1",
|
||||||
|
"expiry-map": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pino-pretty": "^9.1.1",
|
"pino-pretty": "^9.1.1",
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import * as Plugins from "./plugins";
|
||||||
|
|
||||||
const AppPlugin = {
|
const AppPlugin = {
|
||||||
name: "App",
|
name: "App",
|
||||||
register: async (
|
async register(
|
||||||
server: Hapi.Server,
|
server: Hapi.Server,
|
||||||
options: { config: IAppConfig }
|
options: { config: IAppConfig }
|
||||||
): Promise<void> => {
|
): Promise<void> {
|
||||||
// declare our **run-time** plugin dependencies
|
// declare our **run-time** plugin dependencies
|
||||||
// these are runtime only deps, not registration time
|
// these are runtime only deps, not registration time
|
||||||
// ref: https://hapipal.com/best-practices/handling-plugin-dependencies
|
// ref: https://hapipal.com/best-practices/handling-plugin-dependencies
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Boom } from "@hapi/boom";
|
import { Boom } from "@hapi/boom";
|
||||||
import { Server } from "@hapi/hapi";
|
import { Server } from "@hapi/hapi";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
import {
|
import {
|
||||||
proto,
|
proto,
|
||||||
|
|
@ -45,43 +45,38 @@ export const addTransactionCapability = (
|
||||||
? ids.filter((item) => !(item in dict))
|
? ids.filter((item) => !(item in dict))
|
||||||
: ids;
|
: ids;
|
||||||
// only fetch if there are any items to fetch
|
// only fetch if there are any items to fetch
|
||||||
if (idsRequiringFetch.length) {
|
if (idsRequiringFetch.length > 0) {
|
||||||
const result = await state.get(type, idsRequiringFetch);
|
const result = await state.get(type, idsRequiringFetch);
|
||||||
|
|
||||||
transactionCache[type] = transactionCache[type] || {};
|
transactionCache[type] = transactionCache[type] || {};
|
||||||
// @ts-expect-error
|
|
||||||
Object.assign(transactionCache[type], result);
|
Object.assign(transactionCache[type], result);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get: async (type, ids) => {
|
async get(type, ids) {
|
||||||
if (inTransaction) {
|
if (inTransaction) {
|
||||||
await prefetch(type, ids);
|
await prefetch(type, ids);
|
||||||
return ids.reduce((dict, id) => {
|
return ids.reduce((dict, id) => {
|
||||||
const value = transactionCache[type]?.[id];
|
const value = transactionCache[type]?.[id];
|
||||||
if (value) {
|
if (value) {
|
||||||
// @ts-expect-error
|
|
||||||
dict[id] = value;
|
dict[id] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dict;
|
return dict;
|
||||||
}, {});
|
}, {});
|
||||||
} else {
|
|
||||||
return state.get(type, ids);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return state.get(type, ids);
|
||||||
|
|
||||||
},
|
},
|
||||||
set: (data) => {
|
set(data) {
|
||||||
if (inTransaction) {
|
if (inTransaction) {
|
||||||
logger.trace({ types: Object.keys(data) }, "caching in transaction");
|
logger.trace({ types: Object.keys(data) }, "caching in transaction");
|
||||||
for (const key in data) {
|
for (const key in data) {
|
||||||
// @ts-expect-error
|
|
||||||
transactionCache[key] = transactionCache[key] || {};
|
transactionCache[key] = transactionCache[key] || {};
|
||||||
// @ts-expect-error
|
|
||||||
Object.assign(transactionCache[key], data[key]);
|
Object.assign(transactionCache[key], data[key]);
|
||||||
// @ts-expect-error
|
|
||||||
mutations[key] = mutations[key] || {};
|
mutations[key] = mutations[key] || {};
|
||||||
// @ts-expect-error
|
|
||||||
Object.assign(mutations[key], data[key]);
|
Object.assign(mutations[key], data[key]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -90,11 +85,11 @@ export const addTransactionCapability = (
|
||||||
},
|
},
|
||||||
isInTransaction: () => inTransaction,
|
isInTransaction: () => inTransaction,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
prefetch: (type, ids) => {
|
prefetch(type, ids) {
|
||||||
logger.trace({ type, ids }, "prefetching");
|
logger.trace({ type, ids }, "prefetching");
|
||||||
return prefetch(type, ids);
|
return prefetch(type, ids);
|
||||||
},
|
},
|
||||||
transaction: async (work) => {
|
async transaction(work) {
|
||||||
if (inTransaction) {
|
if (inTransaction) {
|
||||||
await work();
|
await work();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -102,7 +97,7 @@ export const addTransactionCapability = (
|
||||||
inTransaction = true;
|
inTransaction = true;
|
||||||
try {
|
try {
|
||||||
await work();
|
await work();
|
||||||
if (Object.keys(mutations).length) {
|
if (Object.keys(mutations).length > 0) {
|
||||||
logger.debug("committing transaction");
|
logger.debug("committing transaction");
|
||||||
await state.set(mutations);
|
await state.set(mutations);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -140,7 +135,7 @@ export const useDatabaseAuthState = (
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
server: Server
|
server: Server
|
||||||
): { state: AuthenticationState; saveState: () => void } => {
|
): { state: AuthenticationState; saveState: () => void } => {
|
||||||
let { logger }: any = server;
|
const { logger }: any = server;
|
||||||
let creds: AuthenticationCreds;
|
let creds: AuthenticationCreds;
|
||||||
let keys: any = {};
|
let keys: any = {};
|
||||||
|
|
||||||
|
|
@ -165,7 +160,7 @@ export const useDatabaseAuthState = (
|
||||||
state: {
|
state: {
|
||||||
creds,
|
creds,
|
||||||
keys: {
|
keys: {
|
||||||
get: (type, ids) => {
|
get(type, ids) {
|
||||||
const key = KEY_MAP[type];
|
const key = KEY_MAP[type];
|
||||||
return ids.reduce((dict, id) => {
|
return ids.reduce((dict, id) => {
|
||||||
let value = keys[key]?.[id];
|
let value = keys[key]?.[id];
|
||||||
|
|
@ -174,18 +169,17 @@ export const useDatabaseAuthState = (
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
value = proto.AppStateSyncKeyData.fromObject(value);
|
value = proto.AppStateSyncKeyData.fromObject(value);
|
||||||
}
|
}
|
||||||
// @ts-expect-error
|
|
||||||
dict[id] = value;
|
dict[id] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dict;
|
return dict;
|
||||||
}, {});
|
}, {});
|
||||||
},
|
},
|
||||||
set: (data) => {
|
set(data) {
|
||||||
for (const _key in data) {
|
for (const _key in data) {
|
||||||
const key = KEY_MAP[_key as keyof SignalDataTypeMap];
|
const key = KEY_MAP[_key as keyof SignalDataTypeMap];
|
||||||
keys[key] = keys[key] || {};
|
keys[key] = keys[key] || {};
|
||||||
// @ts-expect-error
|
|
||||||
Object.assign(keys[key], data[_key]);
|
Object.assign(keys[key], data[_key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import * as Boom from "@hapi/boom";
|
import * as Boom from "@hapi/boom";
|
||||||
import * as Hoek from "@hapi/hoek";
|
import * as Hoek from "@hapi/hoek";
|
||||||
import * as Hapi from "@hapi/hapi";
|
import * as Hapi from "@hapi/hapi";
|
||||||
import { promisify } from "util";
|
import { promisify } from "node:util";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import jwksClient, { hapiJwt2KeyAsync } from "jwks-rsa";
|
import jwksClient, { hapiJwt2KeyAsync } from "jwks-rsa";
|
||||||
import type { IAppConfig } from "../../config";
|
import type { IAppConfig } from "../../config";
|
||||||
|
|
@ -36,22 +36,20 @@ const verifyToken = (settings: any) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCfJwt = (verify: any) => async (
|
const handleCfJwt =
|
||||||
request: Hapi.Request,
|
(verify: any) => async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
||||||
h: Hapi.ResponseToolkit
|
const token = request.headers[CF_JWT_HEADER_NAME];
|
||||||
) => {
|
if (token) {
|
||||||
const token = request.headers[CF_JWT_HEADER_NAME];
|
try {
|
||||||
if (token) {
|
await verify(token);
|
||||||
try {
|
} catch (error) {
|
||||||
await verify(token);
|
console.error(error);
|
||||||
} catch (error) {
|
return Boom.unauthorized("invalid cloudflare access token");
|
||||||
console.error(error);
|
}
|
||||||
return Boom.unauthorized("invalid cloudflare access token");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return h.continue;
|
return h.continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultOpts = {
|
const defaultOpts = {
|
||||||
issuer: undefined,
|
issuer: undefined,
|
||||||
|
|
@ -60,7 +58,10 @@ const defaultOpts = {
|
||||||
validate: undefined,
|
validate: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cfJwtRegister = async (server: Hapi.Server, options: any): Promise<void> => {
|
const cfJwtRegister = async (
|
||||||
|
server: Hapi.Server,
|
||||||
|
options: any
|
||||||
|
): Promise<void> => {
|
||||||
server.dependency(["hapi-auth-jwt2"]);
|
server.dependency(["hapi-auth-jwt2"]);
|
||||||
const settings = Hoek.applyToDefaults(defaultOpts, options);
|
const settings = Hoek.applyToDefaults(defaultOpts, options);
|
||||||
const verify = verifyToken(settings);
|
const verify = verifyToken(settings);
|
||||||
|
|
@ -101,7 +102,7 @@ export const registerCloudflareAccessJwt = async (
|
||||||
options: {
|
options: {
|
||||||
issuer: `https://${domain}`,
|
issuer: `https://${domain}`,
|
||||||
audience,
|
audience,
|
||||||
validate: (decoded: any, _request: any) => {
|
validate(decoded: any, _request: any) {
|
||||||
const { email, name } = decoded;
|
const { email, name } = decoded;
|
||||||
return {
|
return {
|
||||||
isValid: true,
|
isValid: true,
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import type * as Hapi from "@hapi/hapi";
|
import type * as Hapi from "@hapi/hapi";
|
||||||
import NextAuthPlugin, { AdapterFactory } from "@digiresilience/hapi-nextauth";
|
import NextAuthPlugin, { AdapterFactory } from "@digiresilience/hapi-nextauth";
|
||||||
import { NextAuthAdapter } from "@digiresilience/metamigo-common";
|
import { NextAuthAdapter } from "@digiresilience/metamigo-common";
|
||||||
import type { SavedUser, UnsavedUser, SavedSession } from "@digiresilience/metamigo-common";
|
import type {
|
||||||
import { IAppConfig } from "config";
|
SavedUser,
|
||||||
|
UnsavedUser,
|
||||||
|
SavedSession,
|
||||||
|
} from "@digiresilience/metamigo-common";
|
||||||
|
import { IAppConfig } from "@digiresilience/metamigo-config";
|
||||||
|
|
||||||
export const registerNextAuth = async (
|
export const registerNextAuth = async (
|
||||||
server: Hapi.Server,
|
server: Hapi.Server,
|
||||||
40
apps/metamigo-api/src/app/plugins/index.ts
Normal file
40
apps/metamigo-api/src/app/plugins/index.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type * as Hapi from "@hapi/hapi";
|
||||||
|
import { ServerRegisterPluginObjectDirect } from "@hapi/hapi/lib/types/plugin";
|
||||||
|
import type { IInitOptions, IDatabase } from "pg-promise";
|
||||||
|
import Schmervice from "@hapipal/schmervice";
|
||||||
|
import { makePlugin } from "@digiresilience/hapi-pg-promise";
|
||||||
|
|
||||||
|
import type { IAppConfig } from "../../config";
|
||||||
|
import { dbInitOptions, IRepositories } from "@digiresilience/metamigo-db";
|
||||||
|
import { registerNextAuth } from "./hapi-nextauth";
|
||||||
|
import { registerSwagger } from "./swagger";
|
||||||
|
import { registerNextAuthJwt } from "./nextauth-jwt";
|
||||||
|
import { registerCloudflareAccessJwt } from "./cloudflare-jwt";
|
||||||
|
import pg from "pg-promise/typescript/pg-subset";
|
||||||
|
|
||||||
|
export const register = async (
|
||||||
|
server: Hapi.Server,
|
||||||
|
config: IAppConfig
|
||||||
|
): Promise<void> => {
|
||||||
|
await server.register(Schmervice);
|
||||||
|
|
||||||
|
const pgpInit = dbInitOptions(config);
|
||||||
|
const options = {
|
||||||
|
// the only required parameter is the connection string
|
||||||
|
connection: config.db.connection,
|
||||||
|
// ... and the pg-promise initialization options
|
||||||
|
pgpInit,
|
||||||
|
};
|
||||||
|
|
||||||
|
await server.register([
|
||||||
|
{
|
||||||
|
plugin: makePlugin<IInitOptions<IRepositories, pg.IClient>>(),
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await registerNextAuth(server, config);
|
||||||
|
await registerSwagger(server);
|
||||||
|
await registerNextAuthJwt(server, config);
|
||||||
|
await registerCloudflareAccessJwt(server, config);
|
||||||
|
};
|
||||||
|
|
@ -46,7 +46,7 @@ export const registerNextAuthJwt = async (
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
jwkeysB64: [config.nextAuth.signingKey],
|
jwkeysB64: [config.nextAuth.signingKey],
|
||||||
validate: async (decoded, request: Hapi.Request) => {
|
async validate(decoded, request: Hapi.Request) {
|
||||||
const { email, name, role } = decoded;
|
const { email, name, role } = decoded;
|
||||||
const user = await request.db().users.findBy({ email });
|
const user = await request.db().users.findBy({ email });
|
||||||
if (!config.isProd) {
|
if (!config.isProd) {
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import isFunction from "lodash/isFunction";
|
import isFunction from "lodash/isFunction";
|
||||||
import type * as Hapi from "@hapi/hapi";
|
import type * as Hapi from "@hapi/hapi";
|
||||||
import * as RandomRoutes from "./random";
|
|
||||||
import * as UserRoutes from "./users";
|
import * as UserRoutes from "./users";
|
||||||
import * as VoiceRoutes from "./voice";
|
import * as VoiceRoutes from "./voice";
|
||||||
import * as WhatsappRoutes from "./whatsapp";
|
import * as WhatsappRoutes from "./whatsapp";
|
||||||
|
|
@ -25,7 +24,6 @@ export const register = async (server: Hapi.Server): Promise<void> => {
|
||||||
// Load your routes here.
|
// Load your routes here.
|
||||||
// routes are loaded from the list of exported vars
|
// routes are loaded from the list of exported vars
|
||||||
// a route file should export routes directly or an async function that returns the routes.
|
// a route file should export routes directly or an async function that returns the routes.
|
||||||
loadRouteIndex(server, RandomRoutes);
|
|
||||||
loadRouteIndex(server, UserRoutes);
|
loadRouteIndex(server, UserRoutes);
|
||||||
loadRouteIndex(server, VoiceRoutes);
|
loadRouteIndex(server, VoiceRoutes);
|
||||||
loadRouteIndex(server, WhatsappRoutes);
|
loadRouteIndex(server, WhatsappRoutes);
|
||||||
|
|
@ -3,16 +3,14 @@ import * as Joi from "joi";
|
||||||
import * as Helpers from "../helpers";
|
import * as Helpers from "../helpers";
|
||||||
import Boom from "boom";
|
import Boom from "boom";
|
||||||
|
|
||||||
const getSignalService = (request) => {
|
const getSignalService = (request) => request.services("app").signaldService;
|
||||||
return request.services().signaldService;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GetAllSignalBotsRoute = Helpers.withDefaults({
|
export const GetAllSignalBotsRoute = Helpers.withDefaults({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/signal/bots",
|
path: "/api/signal/bots",
|
||||||
options: {
|
options: {
|
||||||
description: "Get all bots",
|
description: "Get all bots",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const signalService = getSignalService(request);
|
const signalService = getSignalService(request);
|
||||||
const bots = await signalService.findAll();
|
const bots = await signalService.findAll();
|
||||||
|
|
||||||
|
|
@ -35,7 +33,7 @@ export const GetBotsRoute = Helpers.noAuth({
|
||||||
path: "/api/signal/bots/{token}",
|
path: "/api/signal/bots/{token}",
|
||||||
options: {
|
options: {
|
||||||
description: "Get one bot",
|
description: "Get one bot",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { token } = request.params;
|
const { token } = request.params;
|
||||||
const signalService = getSignalService(request);
|
const signalService = getSignalService(request);
|
||||||
|
|
||||||
|
|
@ -65,7 +63,7 @@ export const SendBotRoute = Helpers.noAuth({
|
||||||
path: "/api/signal/bots/{token}/send",
|
path: "/api/signal/bots/{token}/send",
|
||||||
options: {
|
options: {
|
||||||
description: "Send a message",
|
description: "Send a message",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { token } = request.params;
|
const { token } = request.params;
|
||||||
const { phoneNumber, message } = request.payload as MessageRequest;
|
const { phoneNumber, message } = request.payload as MessageRequest;
|
||||||
const signalService = getSignalService(request);
|
const signalService = getSignalService(request);
|
||||||
|
|
@ -101,7 +99,7 @@ export const ResetSessionBotRoute = Helpers.noAuth({
|
||||||
path: "/api/signal/bots/{token}/resetSession",
|
path: "/api/signal/bots/{token}/resetSession",
|
||||||
options: {
|
options: {
|
||||||
description: "Reset a session with another user",
|
description: "Reset a session with another user",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { token } = request.params;
|
const { token } = request.params;
|
||||||
const { phoneNumber } = request.payload as ResetSessionRequest;
|
const { phoneNumber } = request.payload as ResetSessionRequest;
|
||||||
const signalService = getSignalService(request);
|
const signalService = getSignalService(request);
|
||||||
|
|
@ -131,7 +129,7 @@ export const ReceiveBotRoute = Helpers.withDefaults({
|
||||||
path: "/api/signal/bots/{token}/receive",
|
path: "/api/signal/bots/{token}/receive",
|
||||||
options: {
|
options: {
|
||||||
description: "Receive messages",
|
description: "Receive messages",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { token } = request.params;
|
const { token } = request.params;
|
||||||
const signalService = getSignalService(request);
|
const signalService = getSignalService(request);
|
||||||
|
|
||||||
|
|
@ -153,7 +151,7 @@ export const RegisterBotRoute = Helpers.withDefaults({
|
||||||
path: "/api/signal/bots/{id}/register",
|
path: "/api/signal/bots/{id}/register",
|
||||||
options: {
|
options: {
|
||||||
description: "Register a bot",
|
description: "Register a bot",
|
||||||
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const signalService = getSignalService(request);
|
const signalService = getSignalService(request);
|
||||||
const { code } = request.query;
|
const { code } = request.query;
|
||||||
|
|
@ -182,7 +180,7 @@ export const CreateBotRoute = Helpers.withDefaults({
|
||||||
path: "/api/signal/bots",
|
path: "/api/signal/bots",
|
||||||
options: {
|
options: {
|
||||||
description: "Register a bot",
|
description: "Register a bot",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { phoneNumber, description } = request.payload as BotRequest;
|
const { phoneNumber, description } = request.payload as BotRequest;
|
||||||
const signalService = getSignalService(request);
|
const signalService = getSignalService(request);
|
||||||
console.log("request.auth.credentials:", request.auth.credentials);
|
console.log("request.auth.credentials:", request.auth.credentials);
|
||||||
|
|
@ -216,7 +214,7 @@ export const RequestCodeRoute = Helpers.withDefaults({
|
||||||
captcha: Joi.string(),
|
captcha: Joi.string(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const { mode, captcha } = request.query;
|
const { mode, captcha } = request.query;
|
||||||
const signalService = getSignalService(request);
|
const signalService = getSignalService(request);
|
||||||
|
|
@ -233,16 +231,20 @@ export const RequestCodeRoute = Helpers.withDefaults({
|
||||||
} else if (mode === "voice") {
|
} else if (mode === "voice") {
|
||||||
await signalService.requestVoiceVerification(bot, captcha);
|
await signalService.requestVoiceVerification(bot, captcha);
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.response().code(200);
|
return h.response().code(200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
if (error.name === "CaptchaRequiredException") {
|
if (error.name === "CaptchaRequiredException") {
|
||||||
return h.response().code(402);
|
return h.response().code(402);
|
||||||
} else if (error.code) {
|
|
||||||
return h.response().code(error.code);
|
|
||||||
} else {
|
|
||||||
return h.response().code(500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.code) {
|
||||||
|
return h.response().code(error.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.response().code(500);
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import * as Joi from "joi";
|
import * as Joi from "joi";
|
||||||
import * as Hapi from "@hapi/hapi";
|
import * as Hapi from "@hapi/hapi";
|
||||||
import { UserRecord, crudRoutesFor, CrudControllerBase } from "@digiresilience/metamigo-common";
|
import {
|
||||||
|
UserRecord,
|
||||||
|
crudRoutesFor,
|
||||||
|
CrudControllerBase,
|
||||||
|
} from "@digiresilience/metamigo-common";
|
||||||
import * as RouteHelpers from "../helpers";
|
import * as RouteHelpers from "../helpers";
|
||||||
|
|
||||||
class UserRecordController extends CrudControllerBase(UserRecord) { }
|
class UserRecordController extends CrudControllerBase(UserRecord) {}
|
||||||
|
|
||||||
const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
|
const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
|
||||||
create: {
|
create: {
|
||||||
|
|
@ -49,11 +53,12 @@ const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UserRoutes = async (
|
export const UserRoutes = RouteHelpers.withDefaults(
|
||||||
_server: Hapi.Server
|
crudRoutesFor(
|
||||||
): Promise<Hapi.ServerRoute[]> => {
|
"user",
|
||||||
const controller = new UserRecordController("users", "userId");
|
"/api/users",
|
||||||
return RouteHelpers.withDefaults(
|
new UserRecordController("users", "userId"),
|
||||||
crudRoutesFor("user", "/api/users", controller, "userId", validator())
|
"userId",
|
||||||
);
|
validator()
|
||||||
};
|
)
|
||||||
|
);
|
||||||
|
|
@ -4,11 +4,14 @@ import * as Boom from "@hapi/boom";
|
||||||
import * as R from "remeda";
|
import * as R from "remeda";
|
||||||
import * as Helpers from "../helpers";
|
import * as Helpers from "../helpers";
|
||||||
import Twilio from "twilio";
|
import Twilio from "twilio";
|
||||||
import { crudRoutesFor, CrudControllerBase } from "@digiresilience/metamigo-common";
|
import {
|
||||||
|
crudRoutesFor,
|
||||||
|
CrudControllerBase,
|
||||||
|
} from "@digiresilience/metamigo-common";
|
||||||
import { VoiceLineRecord, SavedVoiceLine } from "@digiresilience/metamigo-db";
|
import { VoiceLineRecord, SavedVoiceLine } from "@digiresilience/metamigo-db";
|
||||||
|
|
||||||
const TwilioHandlers = {
|
const TwilioHandlers = {
|
||||||
freeNumbers: async (provider, request: Hapi.Request) => {
|
async freeNumbers(provider, request: Hapi.Request) {
|
||||||
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
||||||
const client = Twilio(apiKeySid, apiKeySecret, {
|
const client = Twilio(apiKeySid, apiKeySecret, {
|
||||||
accountSid,
|
accountSid,
|
||||||
|
|
@ -44,23 +47,26 @@ export const VoiceProviderRoutes = Helpers.withDefaults([
|
||||||
providerId: Joi.string().uuid().required(),
|
providerId: Joi.string().uuid().required(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { providerId } = request.params;
|
const { providerId } = request.params;
|
||||||
const voiceProvidersRepo = request.db().voiceProviders;
|
const voiceProvidersRepo = request.db().voiceProviders;
|
||||||
const provider = await voiceProvidersRepo.findById(providerId);
|
const provider = await voiceProvidersRepo.findById(providerId);
|
||||||
if (!provider) return Boom.notFound();
|
if (!provider) return Boom.notFound();
|
||||||
switch (provider.kind) {
|
switch (provider.kind) {
|
||||||
case "TWILIO":
|
case "TWILIO": {
|
||||||
return TwilioHandlers.freeNumbers(provider, request);
|
return TwilioHandlers.freeNumbers(provider, request);
|
||||||
default:
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
return Boom.badImplementation();
|
return Boom.badImplementation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
class VoiceLineRecordController extends CrudControllerBase(VoiceLineRecord) { }
|
class VoiceLineRecordController extends CrudControllerBase(VoiceLineRecord) {}
|
||||||
|
|
||||||
const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
|
const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
|
||||||
create: {
|
create: {
|
||||||
|
|
@ -106,19 +112,14 @@ const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const VoiceLineRoutes = async (
|
export const VoiceLineRoutes = Helpers.withDefaults(
|
||||||
_server: Hapi.Server
|
crudRoutesFor(
|
||||||
): Promise<Hapi.ServerRoute[]> => {
|
"voice-line",
|
||||||
const controller = new VoiceLineRecordController("voiceLines", "id");
|
"/api/voice/voice-line",
|
||||||
return Helpers.withDefaults(
|
new VoiceLineRecordController("voiceLines", "id"),
|
||||||
crudRoutesFor(
|
"id",
|
||||||
"voice-line",
|
validator()
|
||||||
"/api/voice/voice-line",
|
)
|
||||||
controller,
|
);
|
||||||
"id",
|
|
||||||
validator()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export * from "./twilio";
|
export * from "./twilio";
|
||||||
|
|
@ -4,14 +4,13 @@ import * as Boom from "@hapi/boom";
|
||||||
import Twilio from "twilio";
|
import Twilio from "twilio";
|
||||||
import { SavedVoiceProvider } from "@digiresilience/metamigo-db";
|
import { SavedVoiceProvider } from "@digiresilience/metamigo-db";
|
||||||
import pMemoize from "p-memoize";
|
import pMemoize from "p-memoize";
|
||||||
|
import ExpiryMap from "expiry-map";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import * as Helpers from "../../helpers";
|
import * as Helpers from "../../helpers";
|
||||||
import workerUtils from "../../../../worker-utils";
|
import workerUtils from "../../../../worker-utils";
|
||||||
import { SayLanguage, SayVoice } from "twilio/lib/twiml/VoiceResponse";
|
import { SayLanguage, SayVoice } from "twilio/lib/twiml/VoiceResponse";
|
||||||
|
|
||||||
const queueRecording = async (meta) => {
|
const queueRecording = async (meta) => workerUtils.addJob("twilio-recording", meta, { jobKey: meta.callSid });
|
||||||
return workerUtils.addJob("twilio-recording", meta, { jobKey: meta.callSid });
|
|
||||||
};
|
|
||||||
|
|
||||||
const twilioClientFor = (provider: SavedVoiceProvider): Twilio.Twilio => {
|
const twilioClientFor = (provider: SavedVoiceProvider): Twilio.Twilio => {
|
||||||
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
||||||
|
|
@ -43,8 +42,9 @@ const _getOrCreateTTSTestApplication = async (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cache = new ExpiryMap(ms("1h"));
|
||||||
const getOrCreateTTSTestApplication = pMemoize(_getOrCreateTTSTestApplication, {
|
const getOrCreateTTSTestApplication = pMemoize(_getOrCreateTTSTestApplication, {
|
||||||
maxAge: ms("1h"),
|
cache,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TwilioRoutes = Helpers.noAuth([
|
export const TwilioRoutes = Helpers.noAuth([
|
||||||
|
|
@ -58,7 +58,7 @@ export const TwilioRoutes = Helpers.noAuth([
|
||||||
voiceLineId: Joi.string().uuid().required(),
|
voiceLineId: Joi.string().uuid().required(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
|
||||||
const { voiceLineId } = request.params;
|
const { voiceLineId } = request.params;
|
||||||
const voiceLine = await request
|
const voiceLine = await request
|
||||||
.db()
|
.db()
|
||||||
|
|
@ -89,7 +89,7 @@ export const TwilioRoutes = Helpers.noAuth([
|
||||||
voiceLineId: Joi.string().uuid().required(),
|
voiceLineId: Joi.string().uuid().required(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { voiceLineId } = request.params;
|
const { voiceLineId } = request.params;
|
||||||
const { To } = request.payload as { To: string };
|
const { To } = request.payload as { To: string };
|
||||||
const voiceLine = await request.db().voiceLines.findBy({ number: To });
|
const voiceLine = await request.db().voiceLines.findBy({ number: To });
|
||||||
|
|
@ -136,7 +136,7 @@ export const TwilioRoutes = Helpers.noAuth([
|
||||||
voiceLineId: Joi.string().uuid().required(),
|
voiceLineId: Joi.string().uuid().required(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
|
||||||
const { voiceLineId } = request.params;
|
const { voiceLineId } = request.params;
|
||||||
const voiceLine = await request
|
const voiceLine = await request
|
||||||
.db()
|
.db()
|
||||||
|
|
@ -169,7 +169,7 @@ export const TwilioRoutes = Helpers.noAuth([
|
||||||
providerId: Joi.string().uuid().required(),
|
providerId: Joi.string().uuid().required(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { language, voice, prompt } = request.payload as {
|
const { language, voice, prompt } = request.payload as {
|
||||||
language: SayLanguage;
|
language: SayLanguage;
|
||||||
voice: SayVoice;
|
voice: SayVoice;
|
||||||
|
|
@ -192,7 +192,7 @@ export const TwilioRoutes = Helpers.noAuth([
|
||||||
providerId: Joi.string().uuid().required(),
|
providerId: Joi.string().uuid().required(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
|
||||||
const { providerId } = request.params as { providerId: string };
|
const { providerId } = request.params as { providerId: string };
|
||||||
const provider: SavedVoiceProvider = await request
|
const provider: SavedVoiceProvider = await request
|
||||||
.db()
|
.db()
|
||||||
|
|
@ -7,8 +7,8 @@ export const GetAllWhatsappBotsRoute = Helpers.withDefaults({
|
||||||
path: "/api/whatsapp/bots",
|
path: "/api/whatsapp/bots",
|
||||||
options: {
|
options: {
|
||||||
description: "Get all bots",
|
description: "Get all bots",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { whatsappService } = request.services();
|
const { whatsappService } = request.services("app");
|
||||||
|
|
||||||
const bots = await whatsappService.findAll();
|
const bots = await whatsappService.findAll();
|
||||||
|
|
||||||
|
|
@ -31,9 +31,9 @@ export const GetBotsRoute = Helpers.noAuth({
|
||||||
path: "/api/whatsapp/bots/{token}",
|
path: "/api/whatsapp/bots/{token}",
|
||||||
options: {
|
options: {
|
||||||
description: "Get one bot",
|
description: "Get one bot",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { token } = request.params;
|
const { token } = request.params;
|
||||||
const { whatsappService } = request.services();
|
const { whatsappService } = request.services("app");
|
||||||
|
|
||||||
const bot = await whatsappService.findByToken(token);
|
const bot = await whatsappService.findByToken(token);
|
||||||
|
|
||||||
|
|
@ -61,10 +61,10 @@ export const SendBotRoute = Helpers.noAuth({
|
||||||
path: "/api/whatsapp/bots/{token}/send",
|
path: "/api/whatsapp/bots/{token}/send",
|
||||||
options: {
|
options: {
|
||||||
description: "Send a message",
|
description: "Send a message",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { token } = request.params;
|
const { token } = request.params;
|
||||||
const { phoneNumber, message } = request.payload as MessageRequest;
|
const { phoneNumber, message } = request.payload as MessageRequest;
|
||||||
const { whatsappService } = request.services();
|
const { whatsappService } = request.services("app");
|
||||||
|
|
||||||
const bot = await whatsappService.findByToken(token);
|
const bot = await whatsappService.findByToken(token);
|
||||||
|
|
||||||
|
|
@ -93,9 +93,9 @@ export const ReceiveBotRoute = Helpers.withDefaults({
|
||||||
path: "/api/whatsapp/bots/{token}/receive",
|
path: "/api/whatsapp/bots/{token}/receive",
|
||||||
options: {
|
options: {
|
||||||
description: "Receive messages",
|
description: "Receive messages",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { token } = request.params;
|
const { token } = request.params;
|
||||||
const { whatsappService } = request.services();
|
const { whatsappService } = request.services("app");
|
||||||
|
|
||||||
const bot = await whatsappService.findByToken(token);
|
const bot = await whatsappService.findByToken(token);
|
||||||
|
|
||||||
|
|
@ -119,9 +119,9 @@ export const RegisterBotRoute = Helpers.withDefaults({
|
||||||
path: "/api/whatsapp/bots/{id}/register",
|
path: "/api/whatsapp/bots/{id}/register",
|
||||||
options: {
|
options: {
|
||||||
description: "Register a bot",
|
description: "Register a bot",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const { whatsappService } = request.services();
|
const { whatsappService } = request.services("app");
|
||||||
|
|
||||||
const bot = await whatsappService.findById(id);
|
const bot = await whatsappService.findById(id);
|
||||||
|
|
||||||
|
|
@ -146,9 +146,9 @@ export const RefreshBotRoute = Helpers.withDefaults({
|
||||||
path: "/api/whatsapp/bots/{id}/refresh",
|
path: "/api/whatsapp/bots/{id}/refresh",
|
||||||
options: {
|
options: {
|
||||||
description: "Refresh messages",
|
description: "Refresh messages",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const { whatsappService } = request.services();
|
const { whatsappService } = request.services("app");
|
||||||
|
|
||||||
const bot = await whatsappService.findById(id);
|
const bot = await whatsappService.findById(id);
|
||||||
|
|
||||||
|
|
@ -174,9 +174,9 @@ export const CreateBotRoute = Helpers.withDefaults({
|
||||||
path: "/api/whatsapp/bots",
|
path: "/api/whatsapp/bots",
|
||||||
options: {
|
options: {
|
||||||
description: "Register a bot",
|
description: "Register a bot",
|
||||||
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { phoneNumber, description } = request.payload as BotRequest;
|
const { phoneNumber, description } = request.payload as BotRequest;
|
||||||
const { whatsappService } = request.services();
|
const { whatsappService } = request.services("app");
|
||||||
console.log("request.auth.credentials:", request.auth.credentials);
|
console.log("request.auth.credentials:", request.auth.credentials);
|
||||||
|
|
||||||
const bot = await whatsappService.create(
|
const bot = await whatsappService.create(
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import type * as Hapi from "@hapi/hapi";
|
import type * as Hapi from "@hapi/hapi";
|
||||||
import SettingsService from "./settings";
|
import SettingsService from "./settings";
|
||||||
import RandomService from "./random";
|
|
||||||
import WhatsappService from "./whatsapp";
|
import WhatsappService from "./whatsapp";
|
||||||
import SignaldService from "./signald";
|
import SignaldService from "./signald";
|
||||||
|
|
||||||
export const register = async (server: Hapi.Server): Promise<void> => {
|
export const register = async (server: Hapi.Server): Promise<void> => {
|
||||||
// register your services here
|
// register your services here
|
||||||
// don't forget to add them to the AppServices interface in ../types/index.ts
|
// don't forget to add them to the AppServices interface in ../types/index.ts
|
||||||
server.registerService(RandomService);
|
|
||||||
server.registerService(SettingsService);
|
server.registerService(SettingsService);
|
||||||
server.registerService(WhatsappService);
|
server.registerService(WhatsappService);
|
||||||
server.registerService(SignaldService);
|
server.registerService(SignaldService);
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
import { Server } from "@hapi/hapi";
|
import { Server } from "@hapi/hapi";
|
||||||
import { Service } from "@hapipal/schmervice";
|
import { Service } from "@hapipal/schmervice";
|
||||||
import {
|
import {
|
||||||
SignaldAPI,
|
SignaldAPI,
|
||||||
IncomingMessagev1,
|
IncomingMessagev1,
|
||||||
ClientMessageWrapperv1
|
ClientMessageWrapperv1,
|
||||||
} from "@digiresilience/node-signald";
|
} from "@digiresilience/node-signald";
|
||||||
import { SavedSignalBot as Bot } from "@digiresilience/metamigo-db";
|
import { SavedSignalBot as Bot } from "@digiresilience/metamigo-db";
|
||||||
import workerUtils from "../../worker-utils";
|
import workerUtils from "../../worker-utils";
|
||||||
|
|
@ -54,12 +54,15 @@ export default class SignaldService extends Service {
|
||||||
this.signald.on("transport_connected", async () => {
|
this.signald.on("transport_connected", async () => {
|
||||||
this.onConnected();
|
this.onConnected();
|
||||||
});
|
});
|
||||||
this.signald.on("transport_received_payload", async (payload: ClientMessageWrapperv1) => {
|
this.signald.on(
|
||||||
this.server.logger.debug({ payload }, "signald payload received");
|
"transport_received_payload",
|
||||||
if (payload.type === "IncomingMessage") {
|
async (payload: ClientMessageWrapperv1) => {
|
||||||
this.receiveMessage(payload.data)
|
this.server.logger.debug({ payload }, "signald payload received");
|
||||||
|
if (payload.type === "IncomingMessage") {
|
||||||
|
this.receiveMessage(payload.data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
this.signald.on("transport_sent_payload", async (payload) => {
|
this.signald.on("transport_sent_payload", async (payload) => {
|
||||||
this.server.logger.debug({ payload }, "signald payload sent");
|
this.server.logger.debug({ payload }, "signald payload sent");
|
||||||
});
|
});
|
||||||
|
|
@ -163,7 +166,9 @@ export default class SignaldService extends Service {
|
||||||
this.server.logger.error("invalid message received");
|
this.server.logger.error("invalid message received");
|
||||||
}
|
}
|
||||||
|
|
||||||
const bot = await this.server.db().signalBots.findBy({ phoneNumber: account });
|
const bot = await this.server
|
||||||
|
.db()
|
||||||
|
.signalBots.findBy({ phoneNumber: account });
|
||||||
if (!bot) {
|
if (!bot) {
|
||||||
this.server.logger.info("message received for unknown bot", {
|
this.server.logger.info("message received for unknown bot", {
|
||||||
account,
|
account,
|
||||||
|
|
@ -1,16 +1,31 @@
|
||||||
import { Server } from "@hapi/hapi";
|
import { Server } from "@hapi/hapi";
|
||||||
import { Service } from "@hapipal/schmervice";
|
import { Service } from "@hapipal/schmervice";
|
||||||
import { SavedWhatsappBot as Bot } from "@digiresilience/metamigo-db";
|
import { SavedWhatsappBot as Bot } from "@digiresilience/metamigo-db";
|
||||||
import makeWASocket, { DisconnectReason, proto, downloadContentFromMessage, MediaType } from "@adiwajshing/baileys";
|
import makeWASocket, {
|
||||||
|
DisconnectReason,
|
||||||
|
proto,
|
||||||
|
downloadContentFromMessage,
|
||||||
|
MediaType,
|
||||||
|
AnyMessageContent,
|
||||||
|
WAProto,
|
||||||
|
MiscMessageGenerationOptions,
|
||||||
|
} from "@adiwajshing/baileys";
|
||||||
import workerUtils from "../../worker-utils";
|
import workerUtils from "../../worker-utils";
|
||||||
import { useDatabaseAuthState } from "../lib/whatsapp-key-store";
|
import { useDatabaseAuthState } from "../lib/whatsapp-key-store";
|
||||||
import { connect } from "pg-monitor";
|
|
||||||
|
|
||||||
export type AuthCompleteCallback = (error?: string) => void;
|
export type AuthCompleteCallback = (error?: string) => void;
|
||||||
|
|
||||||
|
export type Connection = {
|
||||||
|
end: (error: Error | undefined) => void;
|
||||||
|
sendMessage: (
|
||||||
|
jid: string,
|
||||||
|
content: AnyMessageContent,
|
||||||
|
options?: MiscMessageGenerationOptions
|
||||||
|
) => Promise<WAProto.WebMessageInfo | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
export default class WhatsappService extends Service {
|
export default class WhatsappService extends Service {
|
||||||
connections: { [key: string]: any } = {};
|
connections: { [key: string]: Connection } = {};
|
||||||
loginConnections: { [key: string]: any } = {};
|
|
||||||
|
|
||||||
static browserDescription: [string, string, string] = [
|
static browserDescription: [string, string, string] = [
|
||||||
"Metamigo",
|
"Metamigo",
|
||||||
|
|
@ -31,68 +46,87 @@ export default class WhatsappService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sleep(ms: number): Promise<void> {
|
private async sleep(ms: number): Promise<void> {
|
||||||
console.log(`pausing ${ms}`)
|
console.log(`pausing ${ms}`);
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resetConnections() {
|
private async resetConnections() {
|
||||||
for (const connection of Object.values(this.connections)) {
|
for (const connection of Object.values(this.connections)) {
|
||||||
try {
|
try {
|
||||||
connection.end(null)
|
connection.end(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connections = {};
|
this.connections = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
private createConnection(bot: Bot, server: Server, options: any, authCompleteCallback?: any) {
|
private createConnection(
|
||||||
const { state, saveState } = useDatabaseAuthState(bot, server)
|
bot: Bot,
|
||||||
|
server: Server,
|
||||||
|
options: any,
|
||||||
|
authCompleteCallback?: any
|
||||||
|
) {
|
||||||
|
const { state, saveState } = useDatabaseAuthState(bot, server);
|
||||||
const connection = makeWASocket({ ...options, auth: state });
|
const connection = makeWASocket({ ...options, auth: state });
|
||||||
let pause = 5000;
|
let pause = 5000;
|
||||||
connection.ev.on('connection.update', async (update) => {
|
connection.ev.on("connection.update", async (update) => {
|
||||||
console.log(`Connection updated ${JSON.stringify(update, null, 2)}`)
|
console.log(`Connection updated ${JSON.stringify(update, null, 2)}`);
|
||||||
const { connection: connectionState, lastDisconnect, qr, isNewLogin } = update
|
const {
|
||||||
|
connection: connectionState,
|
||||||
|
lastDisconnect,
|
||||||
|
qr,
|
||||||
|
isNewLogin,
|
||||||
|
} = update;
|
||||||
if (qr) {
|
if (qr) {
|
||||||
console.log('got qr code')
|
console.log("got qr code");
|
||||||
await this.server.db().whatsappBots.updateQR(bot, qr);
|
await this.server.db().whatsappBots.updateQR(bot, qr);
|
||||||
} else if (isNewLogin) {
|
} else if (isNewLogin) {
|
||||||
console.log("got new login")
|
console.log("got new login");
|
||||||
} else if (connectionState === 'open') {
|
} else if (connectionState === "open") {
|
||||||
console.log('opened connection')
|
console.log("opened connection");
|
||||||
} else if (connectionState === "close") {
|
} else if (connectionState === "close") {
|
||||||
console.log('connection closed due to ', lastDisconnect.error)
|
console.log("connection closed due to", lastDisconnect.error);
|
||||||
const disconnectStatusCode = (lastDisconnect?.error as any)?.output?.statusCode
|
const disconnectStatusCode = (lastDisconnect?.error as any)?.output
|
||||||
|
?.statusCode;
|
||||||
if (disconnectStatusCode === DisconnectReason.restartRequired) {
|
if (disconnectStatusCode === DisconnectReason.restartRequired) {
|
||||||
console.log('reconnecting after got new login')
|
console.log("reconnecting after got new login");
|
||||||
const updatedBot = await this.findById(bot.id);
|
const updatedBot = await this.findById(bot.id);
|
||||||
this.createConnection(updatedBot, server, options)
|
this.createConnection(updatedBot, server, options);
|
||||||
authCompleteCallback()
|
authCompleteCallback();
|
||||||
} else if (disconnectStatusCode !== DisconnectReason.loggedOut) {
|
} else if (disconnectStatusCode !== DisconnectReason.loggedOut) {
|
||||||
console.log('reconnecting')
|
console.log("reconnecting");
|
||||||
await this.sleep(pause)
|
await this.sleep(pause);
|
||||||
pause = pause * 2
|
pause *= 2;
|
||||||
this.createConnection(bot, server, options)
|
this.createConnection(bot, server, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
connection.ev.on('chats.set', item => console.log(`recv ${item.chats.length} chats (is latest: ${item.isLatest})`))
|
connection.ev.process(async (events) => {
|
||||||
connection.ev.on('messages.set', item => console.log(`recv ${item.messages.length} messages (is latest: ${item.isLatest})`))
|
if (events["messaging-history.set"]) {
|
||||||
connection.ev.on('contacts.set', item => console.log(`recv ${item.contacts.length} contacts`))
|
const { chats, contacts, messages, isLatest } =
|
||||||
connection.ev.on('messages.upsert', async m => {
|
events["messaging-history.set"];
|
||||||
console.log("messages upsert")
|
console.log(
|
||||||
|
`recv ${chats.length} chats, ${contacts.length} contacts, ${messages.length} msgs (is latest: ${isLatest})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.ev.on("messages.upsert", async (m) => {
|
||||||
|
console.log("messages upsert");
|
||||||
const { messages } = m;
|
const { messages } = m;
|
||||||
if (messages) {
|
if (messages) {
|
||||||
await this.queueUnreadMessages(bot, messages);
|
await this.queueUnreadMessages(bot, messages);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
connection.ev.on('messages.update', m => console.log(m))
|
connection.ev.on("messages.update", (m) => console.log(m));
|
||||||
connection.ev.on('message-receipt.update', m => console.log(m))
|
connection.ev.on("message-receipt.update", (m) => console.log(m));
|
||||||
connection.ev.on('presence.update', m => console.log(m))
|
connection.ev.on("presence.update", (m) => console.log(m));
|
||||||
connection.ev.on('chats.update', m => console.log(m))
|
connection.ev.on("chats.update", (m) => console.log(m));
|
||||||
connection.ev.on('contacts.upsert', m => console.log(m))
|
connection.ev.on("contacts.upsert", (m) => console.log(m));
|
||||||
connection.ev.on('creds.update', saveState)
|
connection.ev.on("creds.update", saveState);
|
||||||
|
|
||||||
this.connections[bot.id] = connection;
|
this.connections[bot.id] = connection;
|
||||||
}
|
}
|
||||||
|
|
@ -103,14 +137,11 @@ export default class WhatsappService extends Service {
|
||||||
const bots = await this.server.db().whatsappBots.findAll();
|
const bots = await this.server.db().whatsappBots.findAll();
|
||||||
for await (const bot of bots) {
|
for await (const bot of bots) {
|
||||||
if (bot.isVerified) {
|
if (bot.isVerified) {
|
||||||
this.createConnection(
|
this.createConnection(bot, this.server, {
|
||||||
bot,
|
browser: WhatsappService.browserDescription,
|
||||||
this.server,
|
printQRInTerminal: false,
|
||||||
{
|
version: [2, 2204, 13],
|
||||||
browser: WhatsappService.browserDescription,
|
});
|
||||||
printQRInTerminal: false,
|
|
||||||
version: [2, 2204, 13],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +157,7 @@ export default class WhatsappService extends Service {
|
||||||
message.imageMessage ||
|
message.imageMessage ||
|
||||||
message.videoMessage;
|
message.videoMessage;
|
||||||
|
|
||||||
let messageContent = Object.values(message)[0]
|
const messageContent = Object.values(message)[0];
|
||||||
let messageType: MediaType;
|
let messageType: MediaType;
|
||||||
let attachment: string;
|
let attachment: string;
|
||||||
let filename: string;
|
let filename: string;
|
||||||
|
|
@ -147,17 +178,21 @@ export default class WhatsappService extends Service {
|
||||||
key.id + "." + message.imageMessage.mimetype.split("/").pop();
|
key.id + "." + message.imageMessage.mimetype.split("/").pop();
|
||||||
mimetype = message.imageMessage.mimetype;
|
mimetype = message.imageMessage.mimetype;
|
||||||
} else if (message.videoMessage) {
|
} else if (message.videoMessage) {
|
||||||
messageType = "video"
|
messageType = "video";
|
||||||
filename =
|
filename =
|
||||||
key.id + "." + message.videoMessage.mimetype.split("/").pop();
|
key.id + "." + message.videoMessage.mimetype.split("/").pop();
|
||||||
mimetype = message.videoMessage.mimetype;
|
mimetype = message.videoMessage.mimetype;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = await downloadContentFromMessage(messageContent, messageType)
|
const stream = await downloadContentFromMessage(
|
||||||
let buffer = Buffer.from([])
|
messageContent,
|
||||||
|
messageType
|
||||||
|
);
|
||||||
|
let buffer = Buffer.from([]);
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
buffer = Buffer.concat([buffer, chunk])
|
buffer = Buffer.concat([buffer, chunk]);
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment = buffer.toString("base64");
|
attachment = buffer.toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,7 +249,12 @@ export default class WhatsappService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(bot: Bot, callback: AuthCompleteCallback): Promise<void> {
|
async register(bot: Bot, callback: AuthCompleteCallback): Promise<void> {
|
||||||
await this.createConnection(bot, this.server, { version: [2, 2204, 13] }, callback);
|
await this.createConnection(
|
||||||
|
bot,
|
||||||
|
this.server,
|
||||||
|
{ version: [2, 2204, 13] },
|
||||||
|
callback
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(bot: Bot, phoneNumber: string, message: string): Promise<void> {
|
async send(bot: Bot, phoneNumber: string, message: string): Promise<void> {
|
||||||
|
|
@ -6,16 +6,10 @@ import type { IAppConfig } from "../../config";
|
||||||
import type { AppDatabase } from "@digiresilience/metamigo-db";
|
import type { AppDatabase } from "@digiresilience/metamigo-db";
|
||||||
|
|
||||||
// add your service interfaces here
|
// add your service interfaces here
|
||||||
interface AppServices {
|
|
||||||
settingsService: ISettingsService;
|
|
||||||
whatsappService: WhatsappService;
|
|
||||||
signaldService: SignaldService;
|
|
||||||
}
|
|
||||||
|
|
||||||
// extend the hapi types with our services and config
|
// extend the hapi types with our services and config
|
||||||
declare module "@hapi/hapi" {
|
declare module "@hapi/hapi" {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
services(): AppServices;
|
|
||||||
db(): AppDatabase;
|
db(): AppDatabase;
|
||||||
pgp: IMain;
|
pgp: IMain;
|
||||||
}
|
}
|
||||||
|
|
@ -25,3 +19,15 @@ declare module "@hapi/hapi" {
|
||||||
pgp: IMain;
|
pgp: IMain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "@hapipal/schmervice" {
|
||||||
|
interface AppServices {
|
||||||
|
settingsService: ISettingsService;
|
||||||
|
whatsappService: WhatsappService;
|
||||||
|
signaldService: SignaldService;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SchmerviceDecorator {
|
||||||
|
(namespace: "app"): AppServices;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
apps/metamigo-api/src/config.ts
Normal file
7
apps/metamigo-api/src/config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export {default, loadConfig, loadConfigRaw, IAppConfig, IAppConvict} from "@digiresilience/metamigo-config";
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { defState } from "@digiresilience/montar";
|
import { defState } from "@digiresilience/montar";
|
||||||
import { configureLogger } from "@digiresilience/metamigo-common";
|
import { configureLogger } from "@digiresilience/metamigo-common";
|
||||||
import config from "config";
|
import config from "@digiresilience/metamigo-config";
|
||||||
|
|
||||||
export const logger = defState("apiLogger", {
|
export const logger = defState("apiLogger", {
|
||||||
start: async () => configureLogger(config),
|
start: async () => configureLogger(config),
|
||||||
|
|
@ -16,9 +16,9 @@ export const deployment = async (
|
||||||
return server;
|
return server;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stopDeployment = async (server: Metamigo.Server): Promise<void> => {
|
export const stopDeployment = async (
|
||||||
return Metamigo.stopDeployment(server);
|
server: Metamigo.Server
|
||||||
};
|
): Promise<void> => Metamigo.stopDeployment(server);
|
||||||
|
|
||||||
const server = defState("server", {
|
const server = defState("server", {
|
||||||
start: () => deployment(config, true),
|
start: () => deployment(config, true),
|
||||||
|
|
@ -9,9 +9,7 @@ const startWorkerUtils = async (): Promise<Worker.WorkerUtils> => {
|
||||||
return workerUtils;
|
return workerUtils;
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopWorkerUtils = async (): Promise<void> => {
|
const stopWorkerUtils = async (): Promise<void> => workerUtils.release();
|
||||||
return workerUtils.release();
|
|
||||||
};
|
|
||||||
|
|
||||||
const workerUtils = defState("apiWorkerUtils", {
|
const workerUtils = defState("apiWorkerUtils", {
|
||||||
start: startWorkerUtils,
|
start: startWorkerUtils,
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
{
|
{
|
||||||
"extends": "../tsconfig.json",
|
"extends": "tsconfig-link",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "build/main",
|
"outDir": "build/main",
|
||||||
"types": ["long", "jest", "node"],
|
"rootDir": "src",
|
||||||
|
"types": ["jest", "node", "long"],
|
||||||
"lib": ["es2020", "DOM"]
|
"lib": ["es2020", "DOM"]
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/.*.ts"],
|
"include": ["src/**/*.ts", "src/**/.*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules/**"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
package-lock.json
generated
55
package-lock.json
generated
|
|
@ -447,6 +447,7 @@
|
||||||
"@hapipal/toys": "^4.0.0",
|
"@hapipal/toys": "^4.0.0",
|
||||||
"blipp": "^4.0.2",
|
"blipp": "^4.0.2",
|
||||||
"camelcase-keys": "^8.0.2",
|
"camelcase-keys": "^8.0.2",
|
||||||
|
"expiry-map": "^2.0.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"graphile-migrate": "^1.4.1",
|
"graphile-migrate": "^1.4.1",
|
||||||
"graphile-worker": "^0.13.0",
|
"graphile-worker": "^0.13.0",
|
||||||
|
|
@ -9921,6 +9922,17 @@
|
||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expiry-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expiry-map/-/expiry-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-K1I5wJe2fiqjyUZf/xhxwTpaopw3F+19DsO7Oggl20+3SVTXDIevVRJav0aBMfposQdkl2E4+gnuOKd3j2X0sA==",
|
||||||
|
"dependencies": {
|
||||||
|
"map-age-cleaner": "^0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ext": {
|
"node_modules/ext": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|
@ -14975,6 +14987,17 @@
|
||||||
"tmpl": "1.0.5"
|
"tmpl": "1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/map-age-cleaner": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-AvxTC6id0fzSf6OyNBTp1syyCuKO7nOJvHgYlhT0Qkkjvk40zZo+av3ayVgXlxnF/DxEzEfY9mMdd7FHsd+wKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"p-defer": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/map-obj": {
|
"node_modules/map-obj": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -16380,6 +16403,14 @@
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/p-defer": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-finally": {
|
"node_modules/p-finally": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -21824,7 +21855,6 @@
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/hapi": "^21.3.0",
|
"@hapi/hapi": "^21.3.0",
|
||||||
"@hapi/hoek": "^11.0.1",
|
|
||||||
"pg-monitor": "^1.4.1",
|
"pg-monitor": "^1.4.1",
|
||||||
"pg-promise": "^10.11.0"
|
"pg-promise": "^10.11.0"
|
||||||
},
|
},
|
||||||
|
|
@ -24902,7 +24932,6 @@
|
||||||
"version": "file:packages/hapi-pg-promise",
|
"version": "file:packages/hapi-pg-promise",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@hapi/hapi": "^21.3.0",
|
"@hapi/hapi": "^21.3.0",
|
||||||
"@hapi/hoek": "^11.0.1",
|
|
||||||
"pg-monitor": "^1.4.1",
|
"pg-monitor": "^1.4.1",
|
||||||
"pg-promise": "^10.11.0"
|
"pg-promise": "^10.11.0"
|
||||||
},
|
},
|
||||||
|
|
@ -30367,6 +30396,14 @@
|
||||||
"jest-util": "^29.5.0"
|
"jest-util": "^29.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"expiry-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expiry-map/-/expiry-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-K1I5wJe2fiqjyUZf/xhxwTpaopw3F+19DsO7Oggl20+3SVTXDIevVRJav0aBMfposQdkl2E4+gnuOKd3j2X0sA==",
|
||||||
|
"requires": {
|
||||||
|
"map-age-cleaner": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ext": {
|
"ext": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -34672,6 +34709,14 @@
|
||||||
"tmpl": "1.0.5"
|
"tmpl": "1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"map-age-cleaner": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-AvxTC6id0fzSf6OyNBTp1syyCuKO7nOJvHgYlhT0Qkkjvk40zZo+av3ayVgXlxnF/DxEzEfY9mMdd7FHsd+wKQ==",
|
||||||
|
"requires": {
|
||||||
|
"p-defer": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"map-obj": {
|
"map-obj": {
|
||||||
"version": "4.3.0"
|
"version": "4.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -34823,6 +34868,7 @@
|
||||||
"blipp": "^4.0.2",
|
"blipp": "^4.0.2",
|
||||||
"camelcase-keys": "^8.0.2",
|
"camelcase-keys": "^8.0.2",
|
||||||
"eslint-config-link": "*",
|
"eslint-config-link": "*",
|
||||||
|
"expiry-map": "^2.0.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"graphile-migrate": "^1.4.1",
|
"graphile-migrate": "^1.4.1",
|
||||||
"graphile-worker": "^0.13.0",
|
"graphile-worker": "^0.13.0",
|
||||||
|
|
@ -35731,6 +35777,11 @@
|
||||||
"orderedmap": {
|
"orderedmap": {
|
||||||
"version": "2.1.0"
|
"version": "2.1.0"
|
||||||
},
|
},
|
||||||
|
"p-defer": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw=="
|
||||||
|
},
|
||||||
"p-finally": {
|
"p-finally": {
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/hapi": "^21.3.0",
|
"@hapi/hapi": "^21.3.0",
|
||||||
"@hapi/hoek": "^11.0.1",
|
|
||||||
"pg-monitor": "^1.4.1",
|
"pg-monitor": "^1.4.1",
|
||||||
"pg-promise": "^10.11.0"
|
"pg-promise": "^10.11.0"
|
||||||
},
|
},
|
||||||
|
|
@ -19,8 +18,7 @@
|
||||||
"fix:lint": "eslint src --ext .ts --fix",
|
"fix:lint": "eslint src --ext .ts --fix",
|
||||||
"fmt": "prettier \"src/**/*.ts\" --write",
|
"fmt": "prettier \"src/**/*.ts\" --write",
|
||||||
"test": "jest --coverage --forceExit --detectOpenHandles --reporters=default --reporters=jest-junit",
|
"test": "jest --coverage --forceExit --detectOpenHandles --reporters=default --reporters=jest-junit",
|
||||||
"lint": "eslint src --ext .ts",
|
"lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different",
|
||||||
"lint-fmt": "prettier \"src/**/*.ts\" --list-different",
|
|
||||||
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
|
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
|
||||||
"watch:build": "tsc -p tsconfig.json -w"
|
"watch:build": "tsc -p tsconfig.json -w"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import * as Hapi from "@hapi/hapi";
|
import * as Hapi from "@hapi/hapi";
|
||||||
import PgPromisePlugin from ".";
|
import { makePlugin } from ".";
|
||||||
|
|
||||||
|
const plugin = makePlugin();
|
||||||
|
|
||||||
describe("plugin option validation", () => {
|
describe("plugin option validation", () => {
|
||||||
let server;
|
let server;
|
||||||
|
|
@ -8,7 +10,7 @@ describe("plugin option validation", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when no connection details defined", async () => {
|
it("should throw when no connection details defined", async () => {
|
||||||
expect(server.register(PgPromisePlugin)).rejects.toThrow();
|
expect(server.register(plugin)).rejects.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -20,7 +22,7 @@ describe("basic plugin runtime", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
server = new Hapi.Server({ port: 0 });
|
server = new Hapi.Server({ port: 0 });
|
||||||
await server.register({
|
await server.register({
|
||||||
plugin: PgPromisePlugin,
|
plugin,
|
||||||
options: defaultOpts,
|
options: defaultOpts,
|
||||||
});
|
});
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
@ -63,7 +65,7 @@ describe("plugin runtime", () => {
|
||||||
expect.assertions(5);
|
expect.assertions(5);
|
||||||
|
|
||||||
await server.register({
|
await server.register({
|
||||||
plugin: PgPromisePlugin,
|
plugin,
|
||||||
options: {
|
options: {
|
||||||
...defaultOpts,
|
...defaultOpts,
|
||||||
logSql: true,
|
logSql: true,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,12 @@
|
||||||
import * as Hapi from "@hapi/hapi";
|
import * as Hapi from "@hapi/hapi";
|
||||||
import * as Hoek from "@hapi/hoek";
|
|
||||||
import pgPromise from "pg-promise";
|
import pgPromise from "pg-promise";
|
||||||
import * as pgMonitor from "pg-monitor";
|
import * as pgMonitor from "pg-monitor";
|
||||||
import type { IConnectionParameters } from "pg-promise/typescript/pg-subset";
|
import type { IConnectionParameters } from "pg-promise/typescript/pg-subset";
|
||||||
import type { IMain, IInitOptions } from "pg-promise";
|
import type { IMain, IInitOptions } from "pg-promise";
|
||||||
|
|
||||||
import { IPGPPluginOptions, ExtendedProtocol } from "./types";
|
import { IPGPPluginOptions, ExtendedProtocol } from "./types";
|
||||||
|
import { Plugin } from "@hapi/hapi/lib/types/plugin";
|
||||||
|
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
const defaultOptions: IPGPPluginOptions<unknown> = {
|
|
||||||
connection: undefined,
|
|
||||||
pgpInit: {},
|
|
||||||
logSql: false,
|
|
||||||
decorateAs: {
|
|
||||||
pgp: "pgp",
|
|
||||||
db: "db",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const startDiagnostics = <T>(
|
export const startDiagnostics = <T>(
|
||||||
logSql: boolean,
|
logSql: boolean,
|
||||||
|
|
@ -45,59 +35,57 @@ const startDb = async <T>(
|
||||||
return db;
|
return db;
|
||||||
};
|
};
|
||||||
|
|
||||||
const register = async <T>(
|
export function makePlugin<T>(): Plugin<IPGPPluginOptions<T>, void> {
|
||||||
server: Hapi.Server,
|
return {
|
||||||
userOpts?: IPGPPluginOptions<T>
|
version: "1.0.0",
|
||||||
): Promise<void> => {
|
name: "pg-promise",
|
||||||
const options: IPGPPluginOptions<T> = Hoek.applyToDefaults(
|
async register(
|
||||||
defaultOptions,
|
server: Hapi.Server,
|
||||||
userOpts
|
userOpts?: IPGPPluginOptions<T>
|
||||||
) as IPGPPluginOptions<T>;
|
): Promise<void> {
|
||||||
|
if (userOpts.logSql === undefined) userOpts.logSql = false;
|
||||||
|
if (!userOpts.decorateAs) userOpts.decorateAs = { pgp: "pgp", db: "db" };
|
||||||
|
|
||||||
if (!options.connection) {
|
const options = userOpts;
|
||||||
throw new Error(
|
|
||||||
"hapi-pg-promise: connection details are not defined. You must specify a valid connection for the plugin to boot."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("pgp" in options && "pgpInit" in options) {
|
if (!options.connection) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"hapi-pg-promise: options pgp and pgpInit are mutually exclusive"
|
"hapi-pg-promise: connection details are not defined. You must specify a valid connection for the plugin to boot."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pgp: IMain;
|
if ("pgp" in options && "pgpInit" in options) {
|
||||||
if ("pgp" in options) {
|
throw new Error(
|
||||||
pgp = options.pgp;
|
"hapi-pg-promise: options pgp and pgpInit are mutually exclusive"
|
||||||
} else {
|
);
|
||||||
pgp = await startPgp(options.pgpInit);
|
}
|
||||||
startDiagnostics(options.logSql, options.pgpInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = await startDb(pgp, options.connection);
|
let pgp: IMain;
|
||||||
|
if ("pgp" in options) {
|
||||||
|
pgp = options.pgp;
|
||||||
|
} else {
|
||||||
|
pgp = await startPgp(options.pgpInit || {});
|
||||||
|
startDiagnostics(options.logSql, options.pgpInit || {});
|
||||||
|
}
|
||||||
|
|
||||||
if (options.decorateAs) {
|
const db = await startDb(pgp, options.connection);
|
||||||
if (options.decorateAs) {
|
|
||||||
server.decorate("request", options.decorateAs.pgp, pgp);
|
|
||||||
server.decorate("server", options.decorateAs.pgp, pgp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.decorateAs.db) {
|
if (options.decorateAs) {
|
||||||
server.decorate("server", options.decorateAs.db, () => db);
|
if (options.decorateAs) {
|
||||||
server.decorate("request", options.decorateAs.db, () => db);
|
server.decorate("request", options.decorateAs.pgp, pgp);
|
||||||
}
|
server.decorate("server", options.decorateAs.pgp, pgp);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.ext("onPostStop", async () => {
|
if (options.decorateAs.db) {
|
||||||
stopDiagnostics();
|
server.decorate("server", options.decorateAs.db, () => db);
|
||||||
await db.$pool.end();
|
server.decorate("request", options.decorateAs.db, () => db);
|
||||||
});
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const pgPromisePlugin = {
|
server.ext("onPostStop", async () => {
|
||||||
register,
|
stopDiagnostics();
|
||||||
name: "pg-promise",
|
await db.$pool.end();
|
||||||
version: "0.0.1",
|
});
|
||||||
};
|
},
|
||||||
|
};
|
||||||
export default pgPromisePlugin;
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
"author": "Abel Luck <abel@guardianproject.info>",
|
"author": "Abel Luck <abel@guardianproject.info>",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"private": false,
|
"private": false,
|
||||||
"main": "dist/index.js",
|
"main": "build/main/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "types/main/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"/dist"
|
"/dist"
|
||||||
],
|
],
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --build --verbose",
|
"build": "tsc --build --verbose",
|
||||||
"watch": "tsc --build --verbose --watch",
|
"watch": "tsc --build --verbose --watch",
|
||||||
"generate": "node util/generate.js && prettier src/generated.ts -w --loglevel error && yarn build",
|
"generate": "node util/generate.js && prettier src/generated.ts -w --loglevel error && npm run build",
|
||||||
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --out dist/docs",
|
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --out dist/docs",
|
||||||
"fix": "echo n/a",
|
"fix": "echo n/a",
|
||||||
"lint": "echo n/a",
|
"lint": "echo n/a",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
|
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
|
||||||
},
|
},
|
||||||
"lint": {},
|
"lint": {},
|
||||||
|
"fix:lint": {},
|
||||||
"fmt": {},
|
"fmt": {},
|
||||||
"deploy": {
|
"deploy": {
|
||||||
"dependsOn": ["build", "test", "lint"]
|
"dependsOn": ["build", "test", "lint"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue