Skip to content

Commit

Permalink
feat: allow sending of basic files through webhooks (#181)
Browse files Browse the repository at this point in the history
  • Loading branch information
zaida04 authored Oct 2, 2022
1 parent 85b933c commit 64b4bb7
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 312 deletions.
7 changes: 7 additions & 0 deletions .changeset/ten-steaks-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@guildedjs/guilded-api-typings": patch
"@guildedjs/rest": patch
"@guildedjs/webhook-client": minor
---

feat: allow sending of basic files through webhooks
1 change: 1 addition & 0 deletions packages/guilded-api-typings/lib/v1/rest/Webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface RESTPostWebhookBody {
embeds?: APIEmbed[];
username?: string;
avatar_url?: string;
payload_json?: Pick<RESTPostWebhookBody, "content" | "avatar_url" | "username" | "embeds">;
}

export type RESTPostWebhookResult = WebhookContentPayload;
18 changes: 16 additions & 2 deletions packages/rest/lib/RestManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import EventEmitter from "events";
import { stringify } from "qs";

const packageDetails = require("../package.json");
import FormData from "form-data";

import { GuildedAPIError } from "./errors/GuildedAPIError";
import { PermissionsError } from "./errors/PermissionsError";
import type { RestOptions } from "./typings";
Expand Down Expand Up @@ -42,12 +44,22 @@ export class RestManager {
): Promise<[Response, Promise<T>]> {
const headers: HeadersInit = {};
if (authenticated) headers.Authorization = `Bearer ${this.token}`;

let body: Buffer | string | undefined = undefined;
if (data.body instanceof FormData) {
body ??= data.body.getBuffer();
Object.assign(headers, { ...data.body.getHeaders() });
} else {
body ??= JSON.stringify(body);
}

const requestOptions = {
body: data.body ? JSON.stringify(data.body) : undefined,
body,
headers: {
"content-type": "application/json",
"User-Agent": `@guildedjs-rest/${packageDetails.version} Node.js v${process.version}`,
...headers,
...data.headers,
},
method: data.method,
};
Expand Down Expand Up @@ -141,7 +153,9 @@ export interface MakeOptions<B = Record<string, any>, Q = RequestBodyObject> {
method: string;
query?: Q;
path: string;
body?: B;
body?: B | FormData;
isFormData?: boolean;
headers?: Record<string, string>;
}
export type JSONB = Record<string, any>;
export type RequestBodyObject = JSONB | undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/rest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"typescript": "4.8.3"
},
"dependencies": {
"form-data": "^4.0.0",
"node-fetch": "2.6.7",
"qs": "^6.10.5"
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions packages/webhook-client/__tests__/file-sending/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { readFileSync } from "fs";

import { WebhookClient } from "../../../lib/index";
const client = new WebhookClient("");

void (async (): Promise<unknown> => {
return client.send({ files: [{ name: "bbb.png", content: readFileSync("./bbb.png") }] });
})();
71 changes: 43 additions & 28 deletions packages/webhook-client/lib/WebhookClient.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { RESTPostWebhookBody, RESTPostWebhookResult } from "@guildedjs/guilded-api-typings";
import type { APIContent } from "@guildedjs/guilded-api-typings/dist/v1/structs/Webhook";
import type { APIContent, APIEmbed, RESTPostWebhookBody, RESTPostWebhookResult } from "@guildedjs/guilded-api-typings";
import { RestManager } from "@guildedjs/rest";
import FormData from "form-data";

import type { Embed } from "./Embed";
import { Embed } from "./Embed";
import { type parsedMessage, parseMessage } from "./messageUtil";
import type { MessageAttachment, MessageContent } from "./util";

export class WebhookClient {
public URL: string;
Expand All @@ -20,7 +21,7 @@ export class WebhookClient {

public constructor(
webhookConnection: string | { id: string; token: string },
{ username, avatarURL }: { username?: string; avatarURL?: string },
{ username, avatarURL }: { username?: string; avatarURL?: string } = {},
) {
if (!webhookConnection) {
throw new TypeError(`Must provide Webhook connection info in either string or object. Received ${webhookConnection}.`);
Expand All @@ -46,34 +47,48 @@ export class WebhookClient {
}

public send(
content: string | RESTPostWebhookBody,
embeds?: Embed[],
options?: { username?: string; avatarURL?: string },
content: MessageContent,
embeds?: (Embed | APIEmbed)[],
options?: { files?: MessageAttachment[]; username?: string; avatarURL?: string },
): Promise<WebhookExecuteResponse> {
return this.rest
.post<RESTPostWebhookResult, RESTPostWebhookBody>(
"",
typeof content === "object"
? content
: {
content,
embeds: embeds?.map((x) => x.toJSON()),
username: options?.username ?? this.username ?? undefined,
avatar_url: options?.avatarURL ?? this.avatarURL ?? undefined,
},
)
.then((data) => {
const parsedContent = parseMessage(data.content);
return {
...data,
content: parsedContent.parsedText,
parsedContent,
rawContent: data.content,
} as WebhookExecuteResponse;
});
const contentIsObject = typeof content === "object";
const resEmbeds = transformEmbedToAPIEmbed((contentIsObject ? content.embeds : embeds) ?? []);
const resFiles = contentIsObject ? content.files : options?.files;

const baseBody: RESTPostWebhookBody = contentIsObject
? {
...content,
embeds: resEmbeds,
}
: {
content,
embeds: resEmbeds,
username: options?.username ?? this.username ?? undefined,
avatar_url: options?.avatarURL ?? this.avatarURL ?? undefined,
};

let body: FormData | RESTPostWebhookBody = baseBody;
const formData = new FormData();
if (resFiles?.length) {
resFiles.forEach((value, index) => formData.append(`files[${index}]`, value.content, { filename: value.name, filepath: value.path }));
formData.append("payload_json", JSON.stringify(baseBody), { contentType: "application/json" });
body = formData;
}

return this.rest.post<RESTPostWebhookResult, RESTPostWebhookBody | FormData>("", body).then((data) => {
const parsedContent = parseMessage(data.content);
return {
...data,
content: parsedContent.parsedText,
parsedContent,
rawContent: data.content,
} as WebhookExecuteResponse;
});
}
}

const transformEmbedToAPIEmbed = (embeds: (Embed | APIEmbed)[]): APIEmbed[] => embeds.map((x) => (x instanceof Embed ? x.toJSON() : x));

export interface WebhookExecuteResponse extends Omit<RESTPostWebhookResult, "content"> {
content: string;
parsedContent: parsedMessage;
Expand Down
10 changes: 10 additions & 0 deletions packages/webhook-client/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { APIEmbed, RESTPostWebhookBody } from "@guildedjs/guilded-api-typings";

import { COLORS } from "./consts";
import type { Embed } from "./Embed";

/**
* Copyright 2015 - 2021 Amish Shah
Expand All @@ -18,3 +21,10 @@ export function resolveColor(color: string | number | [number, number, number]):

return color;
}

export interface MessageAttachment {
content?: Buffer;
name: string;
path?: string;
}
export type MessageContent = (RESTPostWebhookBody & { embeds?: (Embed | APIEmbed)[]; files?: MessageAttachment[] }) | string;
3 changes: 2 additions & 1 deletion packages/webhook-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"typescript": "4.8.3"
},
"dependencies": {
"@guildedjs/rest": "workspace:*"
"@guildedjs/rest": "workspace:*",
"form-data": "^4.0.0"
},
"contributors": [
{
Expand Down
Loading

0 comments on commit 64b4bb7

Please sign in to comment.