metamigo-api: work on making it build

This commit is contained in:
Abel Luck 2023-03-13 14:42:49 +00:00
parent 38e68852d9
commit ef216f7b1c
35 changed files with 407 additions and 322 deletions

View file

@ -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"
}, },
}; };

View file

@ -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);
};

View file

@ -1,10 +0,0 @@
import config, {
loadConfig,
loadConfigRaw,
IAppConfig,
IAppConvict,
} from "config";
export { IAppConvict, IAppConfig, loadConfig, loadConfigRaw };
export default config;

View file

@ -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",

View file

@ -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

View file

@ -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]);
} }

View file

@ -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,10 +36,8 @@ 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]; const token = request.headers[CF_JWT_HEADER_NAME];
if (token) { if (token) {
try { try {
@ -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,

View file

@ -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,

View 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);
};

View file

@ -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) {

View file

@ -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);

View file

@ -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);
} }
}, },
}, },

View file

@ -1,6 +1,10 @@
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) {}
@ -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()
)
); );
};

View file

@ -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,17 +47,20 @@ 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();
} }
}
}, },
}, },
}, },
@ -106,19 +112,14 @@ const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
}, },
}); });
export const VoiceLineRoutes = async ( export const VoiceLineRoutes = Helpers.withDefaults(
_server: Hapi.Server
): Promise<Hapi.ServerRoute[]> => {
const controller = new VoiceLineRecordController("voiceLines", "id");
return Helpers.withDefaults(
crudRoutesFor( crudRoutesFor(
"voice-line", "voice-line",
"/api/voice/voice-line", "/api/voice/voice-line",
controller, new VoiceLineRecordController("voiceLines", "id"),
"id", "id",
validator() validator()
) )
); );
};
export * from "./twilio"; export * from "./twilio";

View file

@ -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()

View file

@ -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(

View file

@ -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);

View file

@ -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(
"transport_received_payload",
async (payload: ClientMessageWrapperv1) => {
this.server.logger.debug({ payload }, "signald payload received"); this.server.logger.debug({ payload }, "signald payload received");
if (payload.type === "IncomingMessage") { if (payload.type === "IncomingMessage") {
this.receiveMessage(payload.data) 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,

View file

@ -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,
this.server,
{
browser: WhatsappService.browserDescription, browser: WhatsappService.browserDescription,
printQRInTerminal: false, printQRInTerminal: false,
version: [2, 2204, 13], 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> {

View file

@ -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;
}
}

View file

@ -0,0 +1,7 @@
export {default, loadConfig, loadConfigRaw, IAppConfig, IAppConvict} from "@digiresilience/metamigo-config";

View file

@ -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),

View file

@ -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),

View file

@ -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,

View file

@ -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
View file

@ -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"
}, },

View file

@ -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"
} }

View file

@ -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,

View file

@ -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,14 +35,18 @@ const startDb = async <T>(
return db; return db;
}; };
const register = async <T>( export function makePlugin<T>(): Plugin<IPGPPluginOptions<T>, void> {
return {
version: "1.0.0",
name: "pg-promise",
async register(
server: Hapi.Server, server: Hapi.Server,
userOpts?: IPGPPluginOptions<T> userOpts?: IPGPPluginOptions<T>
): Promise<void> => { ): Promise<void> {
const options: IPGPPluginOptions<T> = Hoek.applyToDefaults( if (userOpts.logSql === undefined) userOpts.logSql = false;
defaultOptions, if (!userOpts.decorateAs) userOpts.decorateAs = { pgp: "pgp", db: "db" };
userOpts
) as IPGPPluginOptions<T>; const options = userOpts;
if (!options.connection) { if (!options.connection) {
throw new Error( throw new Error(
@ -70,8 +64,8 @@ const register = async <T>(
if ("pgp" in options) { if ("pgp" in options) {
pgp = options.pgp; pgp = options.pgp;
} else { } else {
pgp = await startPgp(options.pgpInit); pgp = await startPgp(options.pgpInit || {});
startDiagnostics(options.logSql, options.pgpInit); startDiagnostics(options.logSql, options.pgpInit || {});
} }
const db = await startDb(pgp, options.connection); const db = await startDb(pgp, options.connection);
@ -92,12 +86,6 @@ const register = async <T>(
stopDiagnostics(); stopDiagnostics();
await db.$pool.end(); await db.$pool.end();
}); });
},
}; };
}
const pgPromisePlugin = {
register,
name: "pg-promise",
version: "0.0.1",
};
export default pgPromisePlugin;

View file

@ -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",

View file

@ -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"]