Skip to content

Allow emitters to return custom data #349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/emitters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type OnSentArgs = {
* @public
* @param args - The arguments object
*/
export type OnMessage = (args: OnMessageArgs) => unknown;
export type OnMessage<Returns> = (args: OnMessageArgs) => Returns;

/**
* @public
Expand Down Expand Up @@ -103,6 +103,11 @@ export type OnMessageArgs = {
context?: boolean,
biz_opaque_callback_data?: string
) => Promise<ServerMessageResponse | Response>;
/**
* Utility function for offloading code from the main thread,
* useful for long running tasks such as AI generation
*/
offload: typeof WhatsAppAPI.offload;
/**
* The WhatsAppAPI instance that emitted the event
*/
Expand All @@ -115,7 +120,7 @@ export type OnMessageArgs = {
* @public
* @param args - The arguments object
*/
export type OnStatus = (args: OnStatusArgs) => unknown;
export type OnStatus<Returns> = (args: OnStatusArgs) => Returns;

/**
* @public
Expand Down Expand Up @@ -157,6 +162,11 @@ export type OnStatusArgs = {
* Arbitrary string included in sent messages
*/
biz_opaque_callback_data?: string;
/**
* Utility function for offloading code from the main thread,
* useful for long running tasks such as AI generation
*/
offload: typeof WhatsAppAPI.offload;
/**
* The raw data from the API
*/
Expand Down
58 changes: 30 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { DEFAULT_API_VERSION } from "./types.js";
/**
* The main API Class
*/
export class WhatsAppAPI {
export class WhatsAppAPI<EmittersReturnType = void> {
//#region Properties
/**
* The API token
Expand Down Expand Up @@ -61,11 +61,6 @@ export class WhatsAppAPI {
* If true, API operations will return the fetch promise instead. Intended for low level debugging.
*/
private parsed: boolean;
/**
* If false, the user functions won't be offloaded from the main event loop.
* Intended for Serverless Environments where the process might be killed after the main function finished.
*/
private offload_functions: boolean;
/**
* If false, the API will be used in a less secure way, removing the need for appSecret. Defaults to true.
*/
Expand All @@ -88,9 +83,9 @@ export class WhatsAppAPI {
* ```
*/
public on: {
message?: OnMessage;
message?: OnMessage<EmittersReturnType>;
sent?: OnSent;
status?: OnStatus;
status?: OnStatus<EmittersReturnType>;
} = {};
//#endregion

Expand Down Expand Up @@ -125,7 +120,6 @@ export class WhatsAppAPI {
webhookVerifyToken,
v,
parsed = true,
offload_functions = true,
secure = true,
ponyfill = {}
}: WhatsAppAPIConstructorArguments) {
Expand Down Expand Up @@ -176,7 +170,6 @@ export class WhatsAppAPI {
}

this.parsed = !!parsed;
this.offload_functions = !!offload_functions;
}

//#region Message Operations
Expand Down Expand Up @@ -273,7 +266,7 @@ export class WhatsAppAPI {
response
};

this.user_function(this.on?.sent, args);
WhatsAppAPI.user_function(this.on?.sent, args);

return response ?? promise;
}
Expand Down Expand Up @@ -733,7 +726,7 @@ export class WhatsAppAPI {
*
* const token = "token";
* const appSecret = "appSecret";
* const Whatsapp = new WhatsAppAPI(NodeNext({ token, appSecret }));
* const Whatsapp = new WhatsAppAPI<number>(NodeNext({ token, appSecret }));
*
* function handler(req, res) {
* if (req.method == "POST") {
Expand All @@ -755,8 +748,10 @@ export class WhatsAppAPI {
* } else res.writeHead(501).end();
* };
*
* Whatsapp.on.message = ({ phoneID, from, message, name }) => {
* Whatsapp.on.message = ({ phoneID, from, message, name, reply, offload }) => {
* console.log(`User ${name} (${from}) sent to bot ${phoneID} a(n) ${message.type}`);
* offload(() => reply(new Text("Hello!")));
* return 202;
* };
*
* const server = createServer(handler);
Expand All @@ -766,19 +761,21 @@ export class WhatsAppAPI {
* @param data - The POSTed data object sent by Whatsapp
* @param raw_body - The raw body of the POST request
* @param signature - The x-hub-signature-256 (all lowercase) header signature sent by Whatsapp
* @returns 200, it's the expected http/s response code
* @returns The emitter's return value, undefined if the corresponding emitter isn't set
* @throws 500 if secure and the appSecret isn't specified
* @throws 501 if secure and crypto.subtle or ponyfill isn't available
* @throws 400 if secure and the raw body is missing
* @throws 401 if secure and the signature is missing
* @throws 401 if secure and the signature doesn't match the hash
* @throws 400 if the POSTed data is not a valid Whatsapp API request
* @throws 500 if the user's callback throws an error
* @throws 200, if the POSTed data is valid but not a message or status update (ignored)
*/
async post(
data: PostData,
raw_body?: string,
signature?: string
): Promise<200> {
): Promise<EmittersReturnType | undefined> {
//Validating the payload
if (this.secure) {
if (!this.appSecret) throw 500;
Expand Down Expand Up @@ -841,10 +838,15 @@ export class WhatsAppAPI {
context ? message.id : undefined,
biz_opaque_callback_data
),
offload: WhatsAppAPI.offload,
Whatsapp: this
};

this.user_function(this.on?.message, args);
try {
return this.on?.message?.(args);
} catch (error) {
throw 500;
}
} else if ("statuses" in value) {
const statuses = value.statuses[0];

Expand All @@ -867,15 +869,20 @@ export class WhatsAppAPI {
pricing,
error,
biz_opaque_callback_data,
offload: WhatsAppAPI.offload,
raw: data
};

this.user_function(this.on?.status, args);
try {
return this.on?.status?.(args);
} catch (error) {
throw 500;
}
}

// If unknown payload, just ignore it
// Facebook doesn't care about your server's opinion

return 200;
throw 200;
}

/**
Expand Down Expand Up @@ -993,27 +1000,22 @@ export class WhatsAppAPI {
* @param f - The user function to call
* @param a - The arguments to pass to the function
*/
private user_function<A, F extends ((...a: A[]) => unknown) | undefined>(
f: F,
private static async user_function<A, F extends (...a: A[]) => unknown>(
f?: F,
...a: A[]
) {
if (f) {
if (this.offload_functions) {
this.offload(f, ...a);
} else {
f(...a);
}
WhatsAppAPI.offload(f, ...a);
}
}

/**
* Offload a function to the next tick of the event loop
*
* @internal
* @param f - The function to offload from the main thread
* @param a - The arguments to pass to the function
*/
private offload<A, F extends (...a: A[]) => unknown>(f: F, ...a: A[]) {
static offload<A, F extends (...a: A[]) => unknown>(f: F, ...a: A[]) {
// Thanks @RahulLanjewar93
Promise.resolve().then(() => f(...a));
}
Expand Down
4 changes: 3 additions & 1 deletion src/middleware/adonis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ export class WhatsAppAPI extends WhatsAppAPIMiddleware {
*/
async handle_post(req: Request) {
try {
return await this.post(
await this.post(
req.body() as PostData,
req.raw() ?? "",
req.header("x-hub-signature-256") ?? ""
);

return 200;
} catch (e) {
// In case who knows what fails ¯\_(ツ)_/¯
return isInteger(e) ? e : 500;
Expand Down
4 changes: 3 additions & 1 deletion src/middleware/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ export class WhatsAppAPI extends WhatsAppAPIMiddleware {
*/
async handle_post(req: Request) {
try {
return this.post(
await this.post(
JSON.parse(req.body ?? "{}"),
req.body,
req.header("x-hub-signature-256")
);

return 200;
} catch (e) {
// In case the JSON.parse fails ¯\_(ツ)_/¯
return isInteger(e) ? e : 500;
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { WhatsAppAPI } from "../index.js";
* The abstract class for the middlewares, it extends the WhatsAppAPI class
* and defines the handle_post and handle_get methods for its childs.
*/
export abstract class WhatsAppAPIMiddleware extends WhatsAppAPI {
export abstract class WhatsAppAPIMiddleware extends WhatsAppAPI<void> {
/**
* This method should be called when the server receives a POST request.
* Each child implements it differently depending on the framework.
Expand Down
4 changes: 3 additions & 1 deletion src/middleware/node-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export class WhatsAppAPI extends WhatsAppAPIMiddleware {

if (typeof signature !== "string") throw 400;

return this.post(JSON.parse(body || "{}"), body, signature);
await this.post(JSON.parse(body || "{}"), body, signature);

return 200;
} catch (e) {
// In case the JSON.parse fails ¯\_(ツ)_/¯
return isInteger(e) ? e : 500;
Expand Down
4 changes: 3 additions & 1 deletion src/middleware/web-standard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ export class WhatsAppAPI extends WhatsAppAPIMiddleware {
try {
const body = await req.text();

return this.post(
await this.post(
JSON.parse(body || "{}"),
body,
req.headers.get("x-hub-signature-256") ?? ""
);

return 200;
} catch (e) {
// In case the JSON.parse fails ¯\_(ツ)_/¯
return isInteger(e) ? e : 500;
Expand Down
5 changes: 0 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ export type TheBasicConstructorArguments = {
* Intended for low level debugging.
*/
parsed?: boolean;
/**
* If false, the user functions won't be offloaded from the main event loop.
* Intended for Serverless Environments where the process might be killed after the main function finished.
*/
offload_functions?: boolean;
/**
* If set to false, none of the API checks will be performed, and it will be used in a less secure way.
*
Expand Down
Loading