Skip to content

Commit 7e50ed4

Browse files
authored
Messaging API - August 2020 update (#251)
* refactor: take toBinary out from postBinary * add two new API * fix test * fix api * fix api & add test * add docs * fix docs
1 parent e98bb5b commit 7e50ed4

File tree

7 files changed

+185
-33
lines changed

7 files changed

+185
-33
lines changed

docs/api-reference/client.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,36 @@ class Client {
9393
created: number;
9494
requestId: string;
9595
}>
96+
createUploadAudienceGroupByFile(uploadAudienceGroup: {
97+
description: string;
98+
isIfaAudience?: boolean;
99+
uploadDescription?: string;
100+
file: Buffer | Readable;
101+
}) : Promise<{
102+
audienceGroupId: number;
103+
type: "UPLOAD";
104+
description: string;
105+
created: number;
106+
}>
96107
updateUploadAudienceGroup(
97108
uploadAudienceGroup: {
98109
audienceGroupId: number;
99110
description?: string;
100111
uploadDescription?: string;
101112
audiences: { id: string }[];
102113
},
114+
// for set request timeout
103115
httpConfig?: Partial<AxiosRequestConfig>,
104-
): Promise<{}>
116+
) : Promise<{}>
117+
createUploadAudienceGroupByFile(
118+
uploadAudienceGroup: {
119+
audienceGroupId: number;
120+
uploadDescription?: string;
121+
file: Buffer | Readable;
122+
},
123+
// for set request timeout
124+
httpConfig?: Partial<AxiosRequestConfig>,
125+
}) : Promise<{}>
105126
createClickAudienceGroup(clickAudienceGroup: {
106127
description: string;
107128
requestId: string;

lib/client.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { Readable } from "stream";
22
import HTTPClient from "./http";
33
import * as Types from "./types";
44
import { AxiosResponse, AxiosRequestConfig } from "axios";
5-
6-
import { ensureJSON, toArray } from "./utils";
5+
import { createMultipartFormData, ensureJSON, toArray } from "./utils";
76

87
type ChatType = "group" | "room";
98
type RequestOption = {
@@ -470,6 +469,25 @@ export default class Client {
470469
return ensureJSON(res);
471470
}
472471

472+
public async createUploadAudienceGroupByFile(uploadAudienceGroup: {
473+
description: string;
474+
isIfaAudience?: boolean;
475+
uploadDescription?: string;
476+
file: Buffer | Readable;
477+
}) {
478+
const file = await this.http.toBuffer(uploadAudienceGroup.file);
479+
const body = createMultipartFormData({ ...uploadAudienceGroup, file });
480+
const res = await this.http.post<{
481+
audienceGroupId: number;
482+
type: "UPLOAD";
483+
description: string;
484+
created: number;
485+
}>(`${DATA_API_PREFIX}/audienceGroup/upload/byFile`, body, {
486+
headers: body.getHeaders(),
487+
});
488+
return ensureJSON(res);
489+
}
490+
473491
public async updateUploadAudienceGroup(
474492
uploadAudienceGroup: {
475493
audienceGroupId: number;
@@ -490,6 +508,26 @@ export default class Client {
490508
return ensureJSON(res);
491509
}
492510

511+
public async updateUploadAudienceGroupByFile(
512+
uploadAudienceGroup: {
513+
audienceGroupId: number;
514+
uploadDescription?: string;
515+
file: Buffer | Readable;
516+
},
517+
// for set request timeout
518+
httpConfig?: Partial<AxiosRequestConfig>,
519+
) {
520+
const file = await this.http.toBuffer(uploadAudienceGroup.file);
521+
const body = createMultipartFormData({ ...uploadAudienceGroup, file });
522+
523+
const res = await this.http.put<{}>(
524+
`${DATA_API_PREFIX}/audienceGroup/upload/byFile`,
525+
body,
526+
{ headers: body.getHeaders(), ...httpConfig },
527+
);
528+
return ensureJSON(res);
529+
}
530+
493531
public async createClickAudienceGroup(clickAudienceGroup: {
494532
description: string;
495533
requestId: string;

lib/http.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -96,29 +96,31 @@ export default class HTTPClient {
9696
return res.data;
9797
}
9898

99+
public async toBuffer(data: Buffer | Readable) {
100+
if (Buffer.isBuffer(data)) {
101+
return data;
102+
} else if (data instanceof Readable) {
103+
return await new Promise<Buffer>((resolve, reject) => {
104+
const buffers: Buffer[] = [];
105+
let size = 0;
106+
data.on("data", (chunk: Buffer) => {
107+
buffers.push(chunk);
108+
size += chunk.length;
109+
});
110+
data.on("end", () => resolve(Buffer.concat(buffers, size)));
111+
data.on("error", reject);
112+
});
113+
} else {
114+
throw new Error("invalid data type for binary data");
115+
}
116+
}
117+
99118
public async postBinary<T>(
100119
url: string,
101120
data: Buffer | Readable,
102121
contentType?: string,
103122
): Promise<T> {
104-
const buffer = await (async (): Promise<Buffer> => {
105-
if (Buffer.isBuffer(data)) {
106-
return data;
107-
} else if (data instanceof Readable) {
108-
return new Promise<Buffer>((resolve, reject) => {
109-
const buffers: Buffer[] = [];
110-
let size = 0;
111-
data.on("data", (chunk: Buffer) => {
112-
buffers.push(chunk);
113-
size += chunk.length;
114-
});
115-
data.on("end", () => resolve(Buffer.concat(buffers, size)));
116-
data.on("error", reject);
117-
});
118-
} else {
119-
throw new Error("invalid data type for postBinary");
120-
}
121-
})();
123+
const buffer = await this.toBuffer(data);
122124

123125
const res = await this.instance.post(url, buffer, {
124126
headers: {

lib/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { JSONParseError } from "./exceptions";
2+
import * as FormData from "form-data";
23

34
export function toArray<T>(maybeArr: T | T[]): T[] {
45
return Array.isArray(maybeArr) ? maybeArr : [maybeArr];
@@ -11,3 +12,18 @@ export function ensureJSON<T>(raw: T): T {
1112
throw new JSONParseError("Failed to parse response body as JSON", raw);
1213
}
1314
}
15+
16+
export function createMultipartFormData(
17+
this: FormData | void,
18+
formBody: Record<string, any>,
19+
): FormData {
20+
const formData = this instanceof FormData ? this : new FormData();
21+
Object.entries(formBody).forEach(([key, value]) => {
22+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
23+
formData.append(key, value);
24+
} else {
25+
formData.append(key, String(value));
26+
}
27+
});
28+
return formData;
29+
}

package-lock.json

Lines changed: 17 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"@types/node": "^14.10.0",
4040
"axios": "^0.20.0",
4141
"body-parser": "^1.19.0",
42-
"file-type": "^15.0.0"
42+
"file-type": "^15.0.0",
43+
"form-data": "^3.0.0"
4344
},
4445
"devDependencies": {
4546
"@types/express": "^4.17.8",

test/client.spec.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
OAUTH_BASE_PREFIX_V2_1,
1313
DATA_API_PREFIX,
1414
} from "../lib/endpoints";
15+
import * as FormData from "form-data";
16+
import { createMultipartFormData } from "../lib/utils";
1517

1618
const pkg = require("../package.json");
1719

@@ -106,6 +108,18 @@ describe("client", () => {
106108
.reply(responseFn);
107109
};
108110

111+
const multipartFormDataMatcher = (expectedBody: Record<string, any>) => (
112+
body: any,
113+
) => {
114+
const decoded = Buffer.from(body, "hex");
115+
const boundary = decoded.toString("utf-8").match(/^--(.+)/)[1];
116+
const data = new FormData();
117+
//@ts-ignore
118+
data._boundary = boundary;
119+
createMultipartFormData.call(data, expectedBody);
120+
return data.getBuffer().compare(decoded) === 0;
121+
};
122+
109123
const mockPut = (
110124
prefix: string,
111125
path: string,
@@ -692,6 +706,34 @@ describe("client", () => {
692706
equal(scope.isDone(), true);
693707
});
694708

709+
it("createUploadAudienceGroupByFile", async () => {
710+
const filepath = join(__dirname, "/helpers/line-icon.png");
711+
const buffer = readFileSync(filepath);
712+
713+
const requestBody = {
714+
description: "audienceGroupName",
715+
isIfaAudience: false,
716+
uploadDescription: "uploadDescription",
717+
file: buffer,
718+
};
719+
720+
const scope = nock(DATA_API_PREFIX, {
721+
reqheaders: {
722+
...interceptionOption.reqheaders,
723+
"content-type": value =>
724+
value.startsWith(`multipart/form-data; boundary=`),
725+
},
726+
})
727+
.post(
728+
"/audienceGroup/upload/byFile",
729+
multipartFormDataMatcher(requestBody),
730+
)
731+
.reply(responseFn);
732+
733+
await client.createUploadAudienceGroupByFile(requestBody);
734+
equal(scope.isDone(), true);
735+
});
736+
695737
it("updateUploadAudienceGroup", async () => {
696738
const requestBody = {
697739
audienceGroupId: 4389303728991,
@@ -716,6 +758,31 @@ describe("client", () => {
716758
equal(scope.isDone(), true);
717759
});
718760

761+
it("updateUploadAudienceGroupByFile", async () => {
762+
const filepath = join(__dirname, "/helpers/line-icon.png");
763+
const buffer = readFileSync(filepath);
764+
const requestBody = {
765+
audienceGroupId: 4389303728991,
766+
uploadDescription: "fileName",
767+
file: buffer,
768+
};
769+
const scope = nock(DATA_API_PREFIX, {
770+
reqheaders: {
771+
...interceptionOption.reqheaders,
772+
"content-type": value =>
773+
value.startsWith(`multipart/form-data; boundary=`),
774+
},
775+
})
776+
.put(
777+
"/audienceGroup/upload/byFile",
778+
multipartFormDataMatcher(requestBody),
779+
)
780+
.reply(responseFn);
781+
782+
await client.updateUploadAudienceGroupByFile(requestBody);
783+
equal(scope.isDone(), true);
784+
});
785+
719786
it("createClickAudienceGroup", async () => {
720787
const requestBody = {
721788
description: "audienceGroupName",
@@ -890,7 +957,7 @@ describe("client", () => {
890957
await client.setRichMenuImage("test_rich_menu_id", null);
891958
ok(false);
892959
} catch (err) {
893-
equal(err.message, "invalid data type for postBinary");
960+
equal(err.message, "invalid data type for binary data");
894961
}
895962
});
896963
});

0 commit comments

Comments
 (0)